react的carousel走马灯(也称轮播图)

277 阅读3分钟

Snipaste_2023-12-19_20-20-47.png

完整代码

//组件部分
// 轮播图
import {
  forwardRef,
  useEffect,
  useState,
  useRef,
} from 'react';
import StyleElement from '@/utils/addRulesIntoStyle';
import styles from './Carousel.less';
import propsInterface from './interface';
import { FaArrowRight,FaArrowLeft } from "react-icons/fa";

const Carousel = forwardRef((props: propsInterface, ref: any) => {
  const {
    children,
    carouselStyle = {},
    change = function () {},
    click = function () {},
    direction = 'horizontal',
    autoplay = false,
    interval = 3000,

    arrow='hover',

    dotsShow = true,
    dotsType = 'dot',
    dotsPlacements = 'bottom',
    dotsStyles = {},
    dotsMouseEnter = true,

    dotBackground = '#8a8793',
    active_dotBackground = '#faf3ef',
  } = props;
  //轮播图的真实长度
  const carouselItemLenght = useRef(0);
  const [carouselItem, setCarouselItem] = useState(() => {
    if (Array.isArray(children)) {
      //传入的dom数组扁平化,防止dom的map循环使传入的children存在多维嵌套
      let new_children = children.flat();
      //记录轮播图的真实长度
      carouselItemLenght.current = new_children.length;
      //复制一组保证轮播图循环播放
      new_children = [...new_children, ...new_children];
      return new_children;
    } else {
      return [children];
    }
  });

  //当前展示index
  const [currentCarouselIndex, set_currentCarouselIndex] = useState(() => {
    return 0;
  });
  const _currentCarouselIndex = useRef(0);
  const autoplayTimer = useRef<undefined | NodeJS.Timeout>(undefined); //自动播放的timer

  let styleElement = useRef(new StyleElement('editStyleElement'));
  let originStyles = useRef<string>(''); //保存carouselContent的原始样式,防止被asideBtn操作污染
  //轮播图的播放方向
  let transformDirection = useRef('translateX');

  useEffect(() => {
    //确定轮播图的播放方向
    if (direction === 'vertical') {
      transformDirection.current =
        direction === 'vertical' ? 'translateY' : 'translateX';
      let carouselContent = document.getElementById('carouselContent');
      carouselContent?.setAttribute('style', 'display:block;');
      originStyles.current = carouselContent?.getAttribute('style') ?? '';
    }

    //自动播放
    openAutoplay();

    //组件销毁时
    return () => {
      if (autoplayTimer.current) {
        clearInterval(autoplayTimer.current);
      }
    };
  }, []);

  //鼠标移入事件
  const mouseEnter = function (e: any) {
    btnShow(true);
    if (autoplayTimer.current) {
      clearInterval(autoplayTimer.current);
    }
  };
  //鼠标移出事件
  const mouseLeave = function (e: any) {
    btnShow(false);
    openAutoplay();
  };
  //开启自动播放
  const openAutoplay = function () {
    if (autoplay) {
      if (autoplayTimer.current) {
        clearInterval(autoplayTimer.current);
      }
      autoplayTimer.current = setInterval(function () {
        carouselChange('insert', 'autoplay');
      }, interval);
    }
  };

  /**
   * 切换轮播图
   * @param type insert向右滚动或者向下滚动(direction相关) ;decrease 向左滚动或者向上滚动
   * @param mode 触发轮播图切换的方式 autoplay自动切换 click手动切换
   * @param step 单次轮播图切换的步数
   */
  const carouselChange = function (
    type: 'insert' | 'decrease',
    mode: 'autoplay' | 'click',
    step: number = 1,
  ) {
    if (step > carouselItemLenght.current) {
      throw '单次轮播图切换步数不可大于轮播图成员数量!';
    }

    let carouselContent = document.getElementById('carouselContent');
    let carouselContentStylesStr: string = originStyles.current;
    //本次移动的keyframes
    let keyframesStr = '';
    //本次移动的开始位置,结束位置
    let transformXFrom = 0;
    let transformXTo = 0;
    //改变index
    if (type === 'insert') {
      transformXFrom = -(_currentCarouselIndex.current * 100);
      _currentCarouselIndex.current = _currentCarouselIndex.current + step;
    } else if (type === 'decrease') {
      transformXFrom =
        -(carouselItemLenght.current + _currentCarouselIndex.current) * 100;
      _currentCarouselIndex.current =
        carouselItemLenght.current + _currentCarouselIndex.current - step;
    }
    transformXTo = -(_currentCarouselIndex.current * 100);

    keyframesStr = `@keyframes carouselContentTransfrom_${_currentCarouselIndex.current}{
        0%{
            transform:${transformDirection.current}(${transformXFrom}%)
        }
        100%{
            transform:${transformDirection.current}(${transformXTo}%)
        }
    }`;
    //替换styles规则
    styleElement.current.replaceRulesInStyle(
      'carouselContentTransfrom_once',
      keyframesStr,
    );

    //animation的keyframes名需要动态切换否则不生效
    carouselContentStylesStr =
      carouselContentStylesStr +
      'animation:carouselContentTransfrom_' +
      _currentCarouselIndex.current +
      ' 0.5s forwards linear;';
    carouselContent?.setAttribute('style', carouselContentStylesStr);

    //当展示的轮播图超过实际轮播图的时候重置
    if (_currentCarouselIndex.current > carouselItemLenght.current - 1) {
      _currentCarouselIndex.current =
        _currentCarouselIndex.current - carouselItemLenght.current;
    }

    set_currentCarouselIndex(_currentCarouselIndex.current);
    //轮播图变化回调
    emitChange({
      currentCarouselIndex,
      mode,
      type,
    });
  };

  /**
   * 轮播图按钮出现事件
   * @param show 是否展示轮播图
   */
  const btnShow = function(show:boolean){
    if(arrow==='always'){
      return
    }
    let asideLeft = document.getElementById("asideLeft");
    let asideRight = document.getElementById("asideRight");
    if(show){
      asideLeft?.style.setProperty ('visibility','visible');
      asideLeft?.style.setProperty ('transform','translateX(10px)');
      asideLeft?.style.setProperty ('transition','0.3s');

      asideRight?.style.setProperty ('visibility','visible');
      asideRight?.style.setProperty ('transform','translateX(-10px)');
      asideRight?.style.setProperty ('transition','0.3s');
    }else{
      asideLeft?.style.setProperty ('transform','translateX(-60px)');
      asideLeft?.style.setProperty ('transition','0.3s');
      asideLeft?.style.setProperty('visibility','hidden');

      asideRight?.style.setProperty ('transform','translateX(60px)');
      asideRight?.style.setProperty ('transition','0.3s');
      asideRight?.style.setProperty('visibility','hidden');
    }

  };

  /**
   * 轮播图变化的emit
   * @param data
   */
  const emitChange = function (data: any) {
    change(data);
  };

  /**
   * 轮播图当前页点击
   */
  const carouselClick = function () {
    emitClick(currentCarouselIndex);
  };
  /**
   * 发出点击事件
   * @param data
   */
  const emitClick = function (data: any) {
    click(data);
  };

  /**
   * 设置默认样式
   */
  const returnCarouselStyle = function () {
    let style = {
      with: '100%',
      height: '100%',
      ...carouselStyle,
    };
    return style;
  };

  /**
   * arrow
   */
  const arrowRender = function(){
    if(arrow==='never'){
      return
    }
    return <div>
       <aside
        className={`${styles.decreaseBtn} ${arrow==='hover'?styles.decreaseBtn_hover:''}`}
        id='asideLeft'
        onClick={() => {
          carouselChange('decrease', 'click');
        }}
      >
        <FaArrowLeft/>
      </aside>
      <aside
        className={`${styles.insertBtn} ${arrow==='hover'?styles.insertBtn_hover:''}`}
        id='asideRight'
        onClick={() => {
          carouselChange('insert', 'click');
        }}
      >
        <FaArrowRight/>
      </aside>
    </div>
  }

  /**
   * 指示点
   */
  const dots = function () {
    if (!dotsShow) {
      return null;
    }
    let dotsArr = [];
    for (let i = 0; i < carouselItemLenght.current; i++) {
      dotsArr.push(
        <li
          key={i}
          className={`${styles.dotItem} ${
            dotsType === 'dot' ? styles.dotCicle : styles.dotLine
          }`}

          style={{
            background:`${currentCarouselIndex===i?active_dotBackground:dotBackground}`
          }}
          onMouseEnter={(mouseEvent) => {
            dotsMouseEnterFun(mouseEvent, i);
          }}
        ></li>,
      );
    }
    return dotsArr;
  };
  //dots鼠标移入事件
  const dotsMouseEnterFun = function (
    mouseEvent: React.MouseEvent,
    dotsIndex: number,
  ) {
    if (dotsMouseEnter) {
      let steps: number = dotsIndex - _currentCarouselIndex.current;
      if (steps > 0) {
        carouselChange('insert', 'click', Math.abs(steps));
      } else {
        carouselChange('decrease', 'click', Math.abs(steps));
      }
    }
  };
  //指示点根据传入的dotsPlacements改变显示位置
  const [dotsPlaceStyles, setdotsPlaceStyles] = useState(() => {
    //默认bottom模式
    if (dotsPlacements === 'bottom') {
      return styles.dotsBottom;
    } else if (dotsPlacements === 'top') {
      return styles.dotsTop;
    } else if (dotsPlacements === 'left') {
      return styles.dotsLeft;
    } else if (dotsPlacements === 'right') {
      return styles.dotsRight;
    }
  });
  const returnDotsStyles = function () {
    return {
      ...dotsStyles,
    };
  };

  return (
    <article
      className={`${styles.carousel}`}
      style={returnCarouselStyle()}
      onMouseEnter={mouseEnter}
      onMouseLeave={mouseLeave}
    >
      {/* children传入参数 */}
      <section className={styles.carouselWindow}>
        <div className={styles.carouselContent} id="carouselContent">
          {carouselItem.map(
            (carouselItem_content: any, carouselItemIndex: any) => {
              return (
                <div
                  className={styles.carouselItem_content}
                  key={carouselItemIndex}
                  onClick={carouselClick}
                >
                  {carouselItem_content}
                </div>
              );
            },
          )}
        </div>
      </section>
      {/* 切换按钮 */}
      {arrowRender()}
      {/* 指示点 */}
      <ul
        className={`${dotsPlaceStyles} ${styles.dots}`}
        style={returnDotsStyles()}
      >
        {dots()}
      </ul>
    </article>
  );
});
export default Carousel;

interface propsInterface {
    /**
     * children中最多map两层dom
     */
    children:Array<JSX.Element|Array<JSX.Element|Array<JSX.Element>>>|JSX.Element,
    carouselStyle?:React.CSSProperties,
    /**
     * 轮播图切换事件
     */
    change?:Function,
    /**
     * 轮播图当前也点击事件
     */
    click?:Function,
    /**
     * 轮播图展示方式
     * horizontal 水平
     * vertical 垂直
     */
    direction?:'horizontal'|'vertical',
    /**
     * 是否自动切换
     */
    autoplay?:boolean,
    interval?:number,//自动切换间隔

    /**
     * arrow
     */
    arrow?:'hover'|'always'|'never'

    /**
     * 指示点
     */
    dotsShow?:boolean,
    dotsType?:'line'|'dot', //指示点样式 (线、点)
    dotsPlacements?:'bottom'|'top'|'left'|'right',
    dotsStyles?:React.CSSProperties,
    dotsMouseEnter?:boolean, //鼠标移入dot是否自动切换

    dotBackground?:string, //指示点颜色
    active_dotBackground?:string, //指示点活动颜色

}

export default propsInterface
//style节点内容
class StyleElement{
    //存放节点要添加的内容
    innerHtml:{[key:string]:string} = {};
    //存放节点
    style:HTMLElement|null = null;
    styleId:string;
    constructor(styleId:string){
        this.styleId = styleId
        this.createStyleElement();
    }
    private createStyleElement(){
        let createStyle = document.getElementById(this.styleId);
        if(!createStyle){
            createStyle =  document.createElement('style');
            createStyle.id = this.styleId;
            createStyle.type = 'text/css';
            document.getElementsByTagName('head')[0].appendChild(createStyle);
        }
        this.style = createStyle;
    }
    private innerHtmlAssemble(){
        let innerHtml_new = '';
        for(let i in this.innerHtml){
            innerHtml_new = innerHtml_new + '\n' + this.innerHtml[i];
        }
        return innerHtml_new;
    }
    addRulesIntoStyle(key:string,newRules:string){
        this.innerHtml[key] = newRules;
        if(this.style){
            this.style.innerHTML = this.innerHtmlAssemble();
        }
    }
    replaceRulesInStyle(key:string,newRules:string){
        this.innerHtml[key] = newRules;
        if(this.style){
            this.style.innerHTML = this.innerHtmlAssemble();
        }
    }
    removeRulesInStyle(key:string){
        delete this.innerHtml.key;
        if(this.style){
            this.style.innerHTML = this.innerHtmlAssemble();
        }
    }
    clearRulesInStyle(){
        this.innerHtml = {};
        if(this.style){
            this.style.innerHTML = this.innerHtmlAssemble();
        }
    }
}
export default StyleElement;

//less部分
.carousel {
  width: 100%;
  height: 100%;
  position: relative;
  overflow: hidden;
}
.carouselWindow {
  width: auto;
  height: 100%;
  overflow: hidden;
}
.carouselContent {
  display: flex;
  height: 100%;
}
.carouselItem_content {
  min-width: 100%;
  height: 100%;
}
.decreaseBtn,
.insertBtn {
  position: absolute;
  top: 45%;
  text-align: center;
  background: #bfc9d4;
  width: 40px;
  height: 40px;
  border-radius: 10%;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content:center;
  cursor: pointer;
}
.decreaseBtn{
  left: 10px;
}
.insertBtn{
  right:10px;
}
.decreaseBtn_hover {
  visibility:hidden;
}
.insertBtn_hover {
  visibility:hidden;
}


.dots{
 position: absolute;
 z-index: 10;
}

.dotsBottom{
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
}
.dotsTop{
    top: 20px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
}
.dotsRight{
    top: 50%;
    left: 20px;
    transform: translateY(-50%);
}
.dotItem{
   width: 10px;
   height: 10px;
   margin: 2px;
}   
.dotLine{
  width:20px;
  height: 5px;
  border-radius: 4px;
}
.dotCicle{
  border-radius: 50%;
}
<section className={styles.left}>
        <Carousel
          change={carouselChange}
          click={carouselClick}
          autoplay={true}
          direction="horizontal"
          carouselStyle={{
            height: '50%',
            borderRadius:'4px'
          }}
        > 
          {carouselItem.map(
            (carouselList_item: string, carouselList_index: number) => {
              return (
                <img
                  key={carouselList_index}
                  className={styles.carouselItem_img}
                  src={carouselList_item}
                ></img>
              );
            },
          )}
        </Carousel>
      </section>