React实现无缝滚动轮播图

实现效果:

 由于是演示代码,我是直接写在了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);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/979430.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何修改安全帽/反光衣检测AI边缘计算智能分析网关V4的IP地址?

TSINGSEE青犀推出的智能分析网关V4&#xff0c;是一款集成了BM1684芯片的高性能AI边缘计算智能硬件。其内置的高性能8核ARM A53处理器&#xff0c;主频可高达2.3GHz&#xff0c;INT8峰值算力更是达到了惊人的17.6Tops。此外&#xff0c;该硬件还预装了近40种AI算法模型&#xf…

一周学会Flask3 Python Web开发-Jinja2模板过滤器使用

锋哥原创的Flask3 Python Web开发 Flask3视频教程&#xff1a; 2025版 Flask3 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili 在Jinja2中&#xff0c;过滤器(filter)是一些可以用来修改和过滤变量值的特殊函数&#xff0c;过滤器和变量用一个竖线 | &a…

华为 VRP 系统简介配置SSH,TELNET远程登录

华为 VRP 系统简介&配置SSH/TELNET远程登录 1.华为 VRP 系统概述 1.1 什么是 VRP VRP&#xff08;Versatile Routing Platform 华为数通设备操作系统&#xff09;是华为公司数据通信产品的通用操作系统平台&#xff0c;从低端到核心的全系列路由器、以太网交换机、业务网…

OAK相机的抗震性测试

在工业环境中&#xff0c;双目视觉相机必须具备与工作环境同等的坚固性。鉴于部分客户会将我们的相机应用于恶劣环境&#xff08;例如安装在重型机械上&#xff09;&#xff0c;我们依据EN 60068-2-6:2008标准对相机进行了振动耐受性测试。 测试涉及的相机型号包括&#xff1a…

【jira】用到几张表

jira用到的几张表 测试计划&#xff0c;测试周期&#xff0c;测试用例&#xff0c;问题记录 1. 测试计划 # 记录表&#xff0c;查计划详情 SELECT ID,issuenum,SUMMARY FROM jiraissue where issuenum 22871# 测试计划下&#xff0c;测试周期&#xff0c;查测试周期id&…

Python PDF文件拆分-详解

目录 使用工具 将PDF按页数拆分 将PDF的每一页拆分为单独的文件 将PDF按指定页数拆分 根据页码范围拆分PDF 根据指定内容拆分PDF 将PDF的一页拆分为多页 在日常生活中&#xff0c;我们常常会遇到大型的PDF文件&#xff0c;这些文件可能难以发送、管理和查阅。将PDF拆分成…

机器学习--(随机森林,线性回归)

一、集成学习方法之随机森林 集成学习的基本思想就是将多个分类器组合&#xff0c;从而实现一个预测效果更好的集成分类器。集成算法可以说从一方面验证了中国的一句老话&#xff1a;三个臭皮匠&#xff0c;赛过诸葛亮。集成算法大致可以分为&#xff1a;Bagging&#xff0c;B…

STM32【3】芯片的底层组成概论

关于单片机的组成 单片机的意思是&#xff0c;小小计算电脑&#xff0c;麻雀虽小&#xff0c;五脏俱全&#xff0c;里面包含了CPU&#xff0c;ROM&#xff0c;RAM&#xff0c;各种外设。 CPU地位最高&#xff0c;可以访问ROM和RAM&#xff0c;Flash&#xff0c;GPIO等外设&…

Elasticsearch:过滤 HNSW 搜索,快速模式

作者&#xff1a;来自 Elastic Benjamin Trent 通过我们的 ACORN-1 算法实现&#xff0c;探索我们对 Apache Lucene 中的 HNSW 向量搜索所做的改进。 多年来&#xff0c;Apache Lucene 和 Elasticsearch 一直支持使用 kNN 查询的过滤搜索&#xff0c;允许用户检索符合指定元数据…

golang安装(1.23.6)

1&#xff0e;切换到安装目录 cd /usr/local 2&#xff0e;下载安装包 wget https://go.dev/dl/go1.23.6.linux-amd64.tar.gz 3&#xff0e;解压安装包 sudo tar -C /usr/local -xzf go1.23.6.linux-amd64.tar.gz 4&#xff0e;配置环境变量 vi /etc/profile export PATH$…

fastadmin 后台商品sku(vue)

先上个效果图 首先先引入vue define([backend], function (Backend) {require.config({paths: {vue: /assets/jeekshopskugoods/libs/vue.min,skuimg: /assets/jeekshopskugoods/js/skuimg,skugoods: /assets/jeekshopskugoods/js/skugoods,layui: /assets/LayuiSpzj/layui/la…

Ecode前后端传值

说明 在泛微 E9 系统开发过程中&#xff0c;使用 Ecode 调用后端接口并进行传值是极为常见且关键的操作。在上一篇文章中&#xff0c;我们探讨了 Ecode 调用后端代码的相关内容&#xff0c;本文将深入剖析在 Ecode 中如何向后端传值&#xff0c;以及后端又该如何处理接收这些值…

【Linux第一弹】Linux基础指令(上)

目录 1.ls指令 1.1 ls使用实例 2.pwd指令 3.cd指令 3.1 cd使用实例 4.touch指令 4.1touch使用实例 5.mkdir指令 5.1mkdir使用实例 6.rmdir指令和rm指令 6.1 rmdir指令使用实例->: 6.2 rm指令使用实例 7.man指令 8.cp指令 8.1 cp 使用实例 9.mv指令 9.1mv使用…

性能测试测试策略制定|知名软件测评机构经验分享

随着互联网产品的普及&#xff0c;产品面对的用户量级也越来越大&#xff0c;能抗住指数级增长的瞬间访问量以及交易量是保障购物体验是否顺畅的至关重要的一环&#xff0c;而我们的性能测试恰恰也是为此而存在的。 性能测试是什么呢&#xff1f;性能测试要怎么测呢&#xff1f…

面试(进阶) —虚拟列表在什么场景使用,如何实现?

面试(进阶) —虚拟列表在什么场景使用&#xff0c;如何实现&#xff1f; 在前端开发中&#xff0c;当需要渲染大量数据时&#xff0c;传统的渲染方式往往会遇到性能瓶颈。一次性将大量数据渲染到DOM中&#xff0c;不仅会导致页面加载缓慢&#xff0c;还可能占用大量内存&#x…

力扣 寻找重复数

二分&#xff0c;双指针&#xff0c;环形链表。 题目 不看完题就是排序后&#xff0c;用两个快慢指针移动&#xff0c;找到相同就返回即可。 class Solution {public int findDuplicate(int[] nums) {Arrays.sort(nums);int l0;int r1;while(r<nums.length){if(nums[l]num…

爱普生汽车用显示控制器IC:ScalerIC,汽车接口IC,相机接口IC

爱普生汽车显示控制器IC&#xff0c;汽车显示控制器芯片可以分为三类&#xff1a;爱普生显示控制芯片Scaler IC &#xff0c;爱普生汽车接口IC&#xff0c;爱普生相机接口IC。下面就给大家分别介绍下这三类芯片的具体型号的特征及用途。 爱普生显示控制芯片 Scaler IC Scaler…

LIGHTRAG: SIMPLE AND FASTRETRIEVAL-AUGMENTED GENERATION

一、现状问题、解决方法 现状问题&#xff1a; 分块处理在促进检索增强生成过程中起着至关重要的作用(Lyu et al.&#xff0c; 2024)&#xff0c;分块可以显著提高信息检索的准确性。 但是RAG系统还有其他的问题限制他们的能力&#xff1a; 1.很多方法是用二维向量表示数据…

React的TSX中如何同时使用CSS模块的类名和字符串类名

1.有两种类名方法 import React from react; import styles from ./index.less; const Home: React.FC () > {return (<div><h1 classNamemain>Welcome to the Home Page</h1><p className{styles.list}>This is a simple home page.</p>&…

防火墙的智能选路与NAT实验

实验拓扑 配置IP 防火墙的安全区域划分 销售部和运维部不能互相访问&#xff0c;采取vlan的方式来进行隔离。 在配置vlan之后 &#xff0c;两个部门将不会通信。 以上是基础配置&#xff0c;只是演示在各个部门不通的情况下&#xff0c;使用什么技术来进行隔离网络&#xff0c;…