实现效果:
由于是演示代码,我是直接写在了App.tsx里面在
文件位置如下:
App.tsx代码如下:
import { useState, useEffect, useCallback, useRef } from "react";
import { ImageContainer } from "./view/ImageContainer";
// 图片列表配置
const IMAGE_LIST = [
"https://oss.cloudhubei.com.cn/cms/release/set35/20241014/9ec7a083198223c49c06e3ebbf8df33c.jpg",
"https://img0.baidu.com/it/u=3231399477,1564831636&fm=253&fmt=auto&app=120&f=JPEG?w=1422&h=800",
"https://q4.itc.cn/images01/20240810/879397b2e3ed4bb8b35be2f272d26b7a.jpeg",
"https://img1.baidu.com/it/u=3484599935,468270965&fm=253&fmt=auto&app=120&f=JPEG?w=1422&h=800",
];
// 常量配置
const TRANSITION_DURATION = 500; // 过渡动画持续时间
const AUTO_PLAY_INTERVAL = 2000; // 自动播放间隔时间
const USER_INTERACTION_DELAY = 1000; // 用户交互后暂停自动播放的时间
function App() {
// 状态管理
const [imgIndex, setImgIndex] = useState<number>(0); // 当前显示的图片索引
const [isAutoPlaying, setIsAutoPlaying] = useState(false); // 是否自动播放
const [isSliding, setIsSliding] = useState(false); // 是否正在滑动
const [direction, setDirection] = useState<"left" | "right">("left"); // 滑动方向
const [isPaused, setIsPaused] = useState(false); // 是否暂停(鼠标悬停时)
const [translateX, setTranslateX] = useState(0); // 滑动距离
const [userInteracting, setUserInteracting] = useState(false); // 用户是否正在交互
const userInteractingTimer = useRef<NodeJS.Timeout | null>(null); // 用户交互定时器
// 处理图片过渡效果
const handleSlideTransition = useCallback(
(nextIndex: number, slideDirection: "left" | "right") => {
if (isSliding) return;
setDirection(slideDirection);
setIsSliding(true);
// 计算每张图片的宽度百分比
const itemWidth = 100 / (IMAGE_LIST.length + 1);
if (nextIndex >= IMAGE_LIST.length) {
// 处理从最后一张到第一张的无缝滚动
setTranslateX(-(nextIndex * itemWidth));
setTimeout(() => {
setIsSliding(false);
setTranslateX(0);
setImgIndex(0);
}, TRANSITION_DURATION);
} else {
// 普通图片切换
setTranslateX(-(nextIndex * itemWidth));
setTimeout(() => {
setImgIndex(nextIndex);
setIsSliding(false);
}, TRANSITION_DURATION);
}
},
[isSliding]
);
// 处理用户交互状态
const handleUserInteraction = useCallback(() => {
setUserInteracting(true);
if (userInteractingTimer.current) {
clearTimeout(userInteractingTimer.current);
}
userInteractingTimer.current = setTimeout(() => {
setUserInteracting(false);
}, USER_INTERACTION_DELAY);
}, []);
// 下一张图片
const handleNext = useCallback(() => {
if (isSliding) return;
handleUserInteraction();
const nextIndex =
imgIndex === IMAGE_LIST.length - 1 ? IMAGE_LIST.length : imgIndex + 1;
handleSlideTransition(nextIndex, "left");
}, [imgIndex, isSliding, handleSlideTransition, handleUserInteraction]);
// 上一张图片
const handlePrevious = useCallback(() => {
if (isSliding) return;
handleUserInteraction();
const previousIndex = imgIndex === 0 ? IMAGE_LIST.length - 1 : imgIndex - 1;
handleSlideTransition(previousIndex, "right");
}, [imgIndex, isSliding, handleSlideTransition, handleUserInteraction]);
// 点击指示器切换图片
const handleDotClick = useCallback(
(index: number) => {
if (isSliding || index === imgIndex) return;
handleUserInteraction();
// 处理特殊情况的无缝滚动
if (imgIndex === IMAGE_LIST.length - 1 && index === 0) {
handleSlideTransition(IMAGE_LIST.length, "left");
} else if (imgIndex === 0 && index === IMAGE_LIST.length - 1) {
handleSlideTransition(index, "right");
} else {
const slideDirection = index > imgIndex ? "left" : "right";
handleSlideTransition(index, slideDirection);
}
},
[imgIndex, isSliding, handleSlideTransition, handleUserInteraction]
);
// 鼠标悬停处理
const handleMouseEnter = useCallback(() => {
if (isAutoPlaying) {
setIsPaused(true);
}
}, [isAutoPlaying]);
const handleMouseLeave = useCallback(() => {
if (isAutoPlaying) {
setIsPaused(false);
}
}, [isAutoPlaying]);
// 切换自动播放状态
const handleToggleAutoPlay = useCallback(() => {
setIsAutoPlaying((prev) => {
if (!prev) {
setUserInteracting(false);
if (userInteractingTimer.current) {
clearTimeout(userInteractingTimer.current);
}
}
return !prev;
});
}, []);
// 初始化
useEffect(() => {
setTranslateX(0);
setImgIndex(0);
}, []);
// 清理定时器
useEffect(() => {
return () => {
if (userInteractingTimer.current) {
clearTimeout(userInteractingTimer.current);
}
};
}, []);
// 自动播放控制
useEffect(() => {
let timer: NodeJS.Timeout;
if (isAutoPlaying && !isSliding && !isPaused && !userInteracting) {
timer = setInterval(() => {
const nextIndex =
imgIndex === IMAGE_LIST.length - 1 ? IMAGE_LIST.length : imgIndex + 1;
handleSlideTransition(nextIndex, "left");
}, AUTO_PLAY_INTERVAL);
}
return () => {
if (timer) {
clearInterval(timer);
}
};
}, [
isAutoPlaying,
isSliding,
isPaused,
userInteracting,
imgIndex,
handleSlideTransition,
]);
return (
<div className={`App ${isSliding ? "sliding" : ""}`}>
<ImageContainer
currentIndex={imgIndex}
onNext={handleNext}
onPrevious={handlePrevious}
onToggleAutoPlay={handleToggleAutoPlay}
isAutoPlaying={isAutoPlaying}
totalImages={IMAGE_LIST.length}
onDotClick={handleDotClick}
imgList={IMAGE_LIST}
direction={direction}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
isSliding={isSliding}
translateX={translateX}
/>
</div>
);
}
export default App;
ImageContainer.tsx代码如下:
import "./ImageContainer.css";
// Props 类型定义
interface PropsType {
currentIndex: number; // 当前显示的图片索引
totalImages: number; // 图片总数
imgList: string[]; // 图片列表
isSliding: boolean; // 是否正在滑动
isAutoPlaying: boolean; // 是否自动播放中
direction: "left" | "right"; // 滑动方向
translateX: number; // 滑动距离
onNext: () => void; // 下一张回调
onPrevious: () => void; // 上一张回调
onToggleAutoPlay: () => void; // 切换自动播放回调
onDotClick: (index: number) => void; // 点击指示器回调
onMouseEnter: () => void; // 鼠标进入回调
onMouseLeave: () => void; // 鼠标离开回调
}
export const ImageContainer = (props: PropsType) => {
// 创建包含额外图片的数组(在末尾添加第一张图片的副本,用于无缝滚动)
const extendedImages = [...props.imgList, props.imgList[0]];
// 计算滑动容器样式
const sliderStyle = {
transform: `translateX(${props.translateX}%)`,
width: `${(props.totalImages + 1) * 100}%`, // 总宽度包含额外的图片
};
// 构建滑动容器的类名
const sliderClassName = [
"image-slider",
props.direction,
props.isSliding ? "sliding" : "",
]
.filter(Boolean)
.join(" ");
// 渲染控制按钮组
const renderControls = () => (
<div className="button-group">
<button className="control-btn" onClick={props.onPrevious}>
上一张
</button>
<button className="control-btn" onClick={props.onToggleAutoPlay}>
{props.isAutoPlaying ? "停止" : "自动播放"}
</button>
<button className="control-btn" onClick={props.onNext}>
下一张
</button>
</div>
);
// 渲染指示器小圆点
const renderDots = () => (
<div className="dots-container">
{Array.from({ length: props.totalImages }).map((_, index) => (
<div
key={index}
className={`dot ${index === props.currentIndex ? "active" : ""}`}
onClick={() => props.onDotClick(index)}
/>
))}
</div>
);
return (
<div
className="image-container"
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
>
{/* 图片滑动容器 */}
<div className={sliderClassName} style={sliderStyle}>
{extendedImages.map((img, index) => (
<img
key={index}
className="fullscreen-img"
src={img}
alt={`slide-${index}`}
style={{ width: `${100 / (props.totalImages + 1)}%` }}
/>
))}
</div>
{/* 控制器容器 */}
<div className="controls-container">
{renderControls()}
{renderDots()}
</div>
</div>
);
};
ImageContainer.css代码如下:
/* 容器样式 */
.image-container {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
background-color: #000;
}
/* 滑动容器样式 */
.image-slider {
position: relative;
height: 100%;
display: flex;
}
/* 滑动过渡效果 */
.image-slider.sliding {
transition: transform 0.5s ease-out;
}
/* 图片样式 */
.fullscreen-img {
height: 100%;
object-fit: cover;
flex-shrink: 0;
}
/* 控制器容器样式 */
.controls-container {
position: fixed;
bottom: 40px;
left: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
z-index: 10;
}
/* 按钮组样式 */
.button-group {
display: flex;
gap: 20px;
}
.control-btn {
padding: 10px 20px;
border: none;
border-radius: 20px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
cursor: pointer;
transition: background-color 0.3s;
font-size: 14px;
}
.control-btn:hover {
background-color: rgba(0, 0, 0, 0.8);
}
/* 指示器样式 */
.dots-container {
display: flex;
gap: 12px;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.5);
cursor: pointer;
transition: all 0.3s;
}
.dot:hover:not(.active) {
background-color: rgba(255, 255, 255, 0.7);
transform: scale(1.1);
}
.dot.active {
background-color: white;
transform: scale(1.1);
}