分别使用javascript , vue3 , React 实现前端轮播图(Carousel)功能
·

1) 纯 JS 实现(单文件,直接打开即可)
<!-- 保存为 carousel-vanilla.html 然后在浏览器打开 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Vanilla JS Carousel</title>
<style>
:root{--w:800px;--h:380px;}
body{font-family:system-ui,Segoe UI,Roboto,Arial;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#f5f7fb}
.carousel{width:clamp(320px,80vw,var(--w)); max-width:100%; position:relative;overflow:hidden;border-radius:8px;background:#111;color:#fff}
.carousel__track{display:flex;transition:transform .45s cubic-bezier(.22,.9,.35,1);will-change:transform}
.carousel__slide{min-width:100%;height:var(--h);display:flex;align-items:center;justify-content:center;font-size:28px;background:#333}
.carousel__slide img{width:100%;height:100%;object-fit:cover;display:block}
.carousel__controls{position:absolute;inset:0;display:flex;align-items:center;justify-content:space-between;padding:0 8px;pointer-events:none}
.carousel__btn{pointer-events:auto;background:rgba(0,0,0,.45);border:0;color:#fff;padding:10px;border-radius:6px;cursor:pointer}
.carousel__indicators{position:absolute;left:50%;bottom:12px;transform:translateX(-50%);display:flex;gap:8px}
.carousel__dot{width:10px;height:10px;border-radius:50%;background:rgba(255,255,255,.45);border:0;cursor:pointer}
.carousel__dot[aria-current="true"]{background:#fff;transform:scale(1.15)}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
</style>
</head>
<body>
<div class="carousel" id="carousel1" tabindex="0" aria-roledescription="carousel" aria-label="示例轮播图">
<div class="carousel__track" data-track>
<div class="carousel__slide"><img src="https://picsum.photos/1200/600?random=1" alt="图片 1"></div>
<div class="carousel__slide"><img src="https://picsum.photos/1200/600?random=2" alt="图片 2"></div>
<div class="carousel__slide"><img src="https://picsum.photos/1200/600?random=3" alt="图片 3"></div>
<div class="carousel__slide"><img src="https://picsum.photos/1200/600?random=4" alt="图片 4"></div>
</div>
<div class="carousel__controls">
<button class="carousel__btn prev" data-prev aria-label="上一张">‹</button>
<button class="carousel__btn next" data-next aria-label="下一张">›</button>
</div>
<div class="carousel__indicators" role="tablist" aria-label="轮播指示器" data-indicators></div>
<div class="sr-only" id="carousel1-status" aria-live="polite"></div>
</div>
<script>
/*
功能:
- 自动播放,悬停/聚焦时暂停
- 上一页/下一页,指示器
- 键盘支持(左/右箭头)
- 响应式(使用 CSS 宽度)
*/
(function(){
class Carousel {
constructor(root, options = {}) {
this.root = root;
this.track = root.querySelector('[data-track]');
this.indicatorsWrap = root.querySelector('[data-indicators]');
this.slides = Array.from(this.track.children);
this.total = this.slides.length;
this.index = 0;
this.interval = options.interval ?? 3000;
this.timer = null;
this.paused = false;
this.statusBox = root.querySelector('#' + (root.id ? root.id + '-status' : 'carousel1-status')) || null;
this.init();
}
init() {
this.buildIndicators();
this.update();
this.attachEvents();
this.start();
}
buildIndicators() {
this.indicatorsWrap.innerHTML = '';
this.dots = this.slides.map((_, i) => {
const btn = document.createElement('button');
btn.className = 'carousel__dot';
btn.type = 'button';
btn.setAttribute('aria-label', `切换到第 ${i+1} 张`);
btn.dataset.index = i;
btn.addEventListener('click', () => this.go(i));
this.indicatorsWrap.appendChild(btn);
return btn;
});
}
update() {
const offset = -this.index * 100;
this.track.style.transform = `translateX(${offset}%)`;
this.dots.forEach((d, i) => d.setAttribute('aria-current', i===this.index ? 'true' : 'false'));
if(this.statusBox){
this.statusBox.textContent = `第 ${this.index+1} 张,共 ${this.total} 张`;
}
}
next() { this.go((this.index + 1) % this.total); }
prev() { this.go((this.index - 1 + this.total) % this.total); }
go(i) { this.index = i; this.update(); }
start() {
this.stop();
this.timer = setInterval(()=> { if(!this.paused) this.next(); }, this.interval);
}
stop() { if(this.timer) { clearInterval(this.timer); this.timer = null; } }
attachEvents() {
this.root.addEventListener('mouseenter', ()=> { this.paused = true; });
this.root.addEventListener('mouseleave', ()=> { this.paused = false; });
this.root.addEventListener('focusin', ()=> { this.paused = true; });
this.root.addEventListener('focusout', ()=> { this.paused = false; });
const prevBtn = this.root.querySelector('[data-prev]');
const nextBtn = this.root.querySelector('[data-next]');
prevBtn?.addEventListener('click', ()=> this.prev());
nextBtn?.addEventListener('click', ()=> this.next());
// keyboard
this.root.addEventListener('keydown', (e)=> {
if(e.key === 'ArrowLeft') { e.preventDefault(); this.prev(); }
if(e.key === 'ArrowRight') { e.preventDefault(); this.next(); }
});
// swipe support (basic)
let startX = null, startTime = null;
this.root.addEventListener('pointerdown', (e)=> {
startX = e.clientX; startTime = Date.now(); this.paused = true;
}, {passive:true});
this.root.addEventListener('pointerup', (e)=> {
if (startX === null) return;
const dx = e.clientX - startX;
const dt = Date.now() - startTime;
// quick swipe threshold
if (Math.abs(dx) > 40 && dt < 500) {
if (dx > 0) this.prev(); else this.next();
}
startX = null; startTime = null; this.paused = false;
});
}
}
// init
document.addEventListener('DOMContentLoaded', ()=> {
const el = document.getElementById('carousel1');
if(el) new Carousel(el, { interval: 3000 });
});
})();
</script>
</body>
</html>
说明:
-
把每张 slide 放在
.carousel__track内,CSS 用translateX做切换。 -
支持鼠标悬停或焦点时暂停、支持键盘、简单手势滑动。
-
图片用
picsum.photos占位,可替换为真实图片 URL。
2) Vue 3 实现(单文件组件风格,<script setup>)
把代码放入 .vue 文件(如 Carousel.vue),在 Vue 3 项目中使用
<!-- Carousel.vue -->
<template>
<div class="carousel" ref="root" tabindex="0" role="region" :aria-label="ariaLabel">
<div class="track" :style="trackStyle" @pointerdown="onPointerDown">
<div v-for="(item, i) in items" :key="item.id" class="slide">
<img :src="item.src" :alt="item.alt || `Slide ${i+1}`" />
</div>
</div>
<div class="controls">
<button class="btn" @click="prev" aria-label="Previous slide">‹</button>
<button class="btn" @click="next" aria-label="Next slide">›</button>
</div>
<div class="indicators" role="tablist" aria-label="Slides">
<button v-for="(s, i) in items" :key="s.id" :aria-current="i===index" @click="go(i)" class="dot" :aria-label="`Go to slide ${i+1}`"></button>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
const props = defineProps({
items: { type: Array, required: true }, // [{id, src, alt}]
interval: { type: Number, default: 3000 },
ariaLabel: { type: String, default: 'Carousel' },
});
const index = ref(0);
const root = ref(null);
let timer = null;
const paused = ref(false);
const trackStyle = computed(() => ({ transform: `translateX(${-index.value * 100}%)`, transition: 'transform .45s cubic-bezier(.22,.9,.35,1)'}));
function next(){ index.value = (index.value + 1) % props.items.length; }
function prev(){ index.value = (index.value - 1 + props.items.length) % props.items.length; }
function go(i){ index.value = i; }
function start(){
stop();
timer = setInterval(()=>{ if(!paused.value) next(); }, props.interval);
}
function stop(){ if(timer){ clearInterval(timer); timer = null; } }
onMounted(()=> {
start();
// mouse / focus pause
root.value?.addEventListener('mouseenter', ()=> paused.value = true);
root.value?.addEventListener('mouseleave', ()=> paused.value = false);
root.value?.addEventListener('focusin', ()=> paused.value = true);
root.value?.addEventListener('focusout', ()=> paused.value = false);
root.value?.addEventListener('keydown', (e)=>{
if(e.key === 'ArrowLeft'){ e.preventDefault(); prev(); }
if(e.key === 'ArrowRight'){ e.preventDefault(); next(); }
});
});
onBeforeUnmount(()=> {
stop();
});
// simple swipe
let startX = null, startTime = null;
function onPointerDown(e){
startX = e.clientX; startTime = Date.now();
const up = (ev) => {
const dx = ev.clientX - startX;
const dt = Date.now() - startTime;
if(Math.abs(dx) > 40 && dt < 500){
if(dx > 0) prev(); else next();
}
startX = null;
root.value.removeEventListener('pointerup', up);
};
root.value.addEventListener('pointerup', up, { once: true });
}
</script>
<style scoped>
.carousel{ width:100%; max-width:900px; margin:0 auto; position:relative; overflow:hidden; border-radius:8px; background:#222 }
.track{ display:flex; width:100%; }
.slide{ min-width:100%; height:420px; display:flex; align-items:center; justify-content:center; }
.slide img{ width:100%; height:100%; object-fit:cover; display:block; }
.controls{ position:absolute; inset:0; display:flex; justify-content:space-between; align-items:center; padding:0 10px; pointer-events:none; }
.btn{ pointer-events:auto; background:rgba(0,0,0,.45); border:0; color:#fff; padding:8px 12px; border-radius:6px; cursor:pointer }
.indicators{ position:absolute; left:50%; transform:translateX(-50%); bottom:14px; display:flex; gap:8px }
.dot{ width:10px; height:10px; border-radius:50%; background:rgba(255,255,255,.45); border:0; cursor:pointer }
.dot[aria-current="true"]{ background:#fff; transform:scale(1.15) }
</style>
使用:
<template>
<Carousel :items="slides" :interval="2500" aria-label="Demo carousel"/>
</template>
<script setup>
import Carousel from './Carousel.vue';
const slides = [
{ id: 1, src: 'https://picsum.photos/1200/600?random=11' },
{ id: 2, src: 'https://picsum.photos/1200/600?random=12' },
{ id: 3, src: 'https://picsum.photos/1200/600?random=13' }
];
</script>
说明(Vue):
-
使用组合式 API(
script setup),props接收 slides 数据。 -
支持 pause on hover/focus、键盘导航、指示点、触摸滑动。
-
可扩展性好:你可以传动画类型、loop、lazyLoad 等。
3) React 实现(函数组件 + Hook)
一个独立组件 Carousel.jsx,以及最小 App.jsx 使用示例。把它放到 React 项目中即可
// Carousel.jsx
import React, { useEffect, useRef, useState } from 'react';
import './carousel.css'; // see CSS below or inline styles
export default function Carousel({ items = [], interval = 3000, ariaLabel = 'Carousel' }) {
const [index, setIndex] = useState(0);
const [paused, setPaused] = useState(false);
const timerRef = useRef(null);
const rootRef = useRef(null);
useEffect(() => {
start();
const r = rootRef.current;
const onKey = (e) => {
if (e.key === 'ArrowLeft') setIndex(i => (i - 1 + items.length) % items.length);
if (e.key === 'ArrowRight') setIndex(i => (i + 1) % items.length);
};
r?.addEventListener('keydown', onKey);
return () => {
stop();
r?.removeEventListener('keydown', onKey);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [items.length]);
useEffect(() => {
if (!paused) start(); else stop();
return () => stop();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [paused, index]);
function start() {
stop();
timerRef.current = setInterval(() => {
setIndex(i => (i + 1) % items.length);
}, interval);
}
function stop() {
if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
}
function prev() { setIndex(i => (i - 1 + items.length) % items.length); }
function next() { setIndex(i => (i + 1) % items.length); }
function go(i) { setIndex(i); }
// swipe
useEffect(() => {
const el = rootRef.current;
if (!el) return;
let startX = null, startTime = null;
const down = (e) => { startX = e.clientX; startTime = Date.now(); setPaused(true); };
const up = (e) => {
if (startX === null) { setPaused(false); return; }
const dx = e.clientX - startX; const dt = Date.now() - startTime;
if (Math.abs(dx) > 40 && dt < 500) { if (dx > 0) prev(); else next(); }
startX = null; setPaused(false);
};
el.addEventListener('pointerdown', down);
el.addEventListener('pointerup', up);
return () => { el.removeEventListener('pointerdown', down); el.removeEventListener('pointerup', up); };
}, [items.length]);
return (
<div className="carousel" ref={rootRef} tabIndex={0} aria-roledescription="carousel" aria-label={ariaLabel}
onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}
onFocus={() => setPaused(true)} onBlur={() => setPaused(false)}>
<div className="track" style={{ transform: `translateX(${-index * 100}%)`, transition: 'transform .45s cubic-bezier(.22,.9,.35,1)'}}>
{items.map((it, i) => (
<div className="slide" key={it.id || i}><img src={it.src} alt={it.alt || `Slide ${i+1}`} /></div>
))}
</div>
<div className="controls">
<button className="btn" onClick={prev} aria-label="Previous">‹</button>
<button className="btn" onClick={next} aria-label="Next">›</button>
</div>
<div className="indicators" role="tablist" aria-label="Slides">
{items.map((_, i) => <button key={i} className="dot" aria-current={i===index} onClick={() => go(i)} aria-label={`Go to slide ${i+1}`}></button>)}
</div>
</div>
);
}
/* carousel.css */
.carousel{ width:100%; max-width:900px; margin:20px auto; position:relative; overflow:hidden; border-radius:8px; background:#222 }
.track{ display:flex; width:100%; }
.slide{ min-width:100%; height:420px; display:flex; align-items:center; justify-content:center; }
.slide img{ width:100%; height:100%; object-fit:cover; display:block; }
.controls{ position:absolute; inset:0; display:flex; justify-content:space-between; align-items:center; padding:0 10px; pointer-events:none; }
.btn{ pointer-events:auto; background:rgba(0,0,0,.45); border:0; color:#fff; padding:8px 12px; border-radius:6px; cursor:pointer }
.indicators{ position:absolute; left:50%; transform:translateX(-50%); bottom:14px; display:flex; gap:8px }
.dot{ width:10px; height:10px; border-radius:50%; background:rgba(255,255,255,.45); border:0; cursor:pointer }
.dot[aria-current="true"]{ background:#fff; transform:scale(1.15) }
使用示例(App.jsx):
// App.jsx
import React from 'react';
import Carousel from './Carousel';
import './carousel.css';
export default function App(){
const slides = [
{ id: 1, src: 'https://picsum.photos/1200/600?random=21' },
{ id: 2, src: 'https://picsum.photos/1200/600?random=22' },
{ id: 3, src: 'https://picsum.photos/1200/600?random=23' },
];
return (
<div style={{padding:20}}>
<h2>React Carousel Demo</h2>
<Carousel items={slides} interval={2500} />
</div>
);
}
说明(React):
-
使用
useEffect管控定时器和事件绑定,做好 cleanup -
暂停逻辑使用组件内 state(hover/focus)
-
支持键盘控制与触摸滑动
更多推荐


所有评论(0)