reactHooks,纯原生定时器实现无限滚动

256 阅读5分钟

效果

tutieshi_640x360_15s.gif

ps1:点击图片打开的页面(GoodsInfo组件),这里不提供。自行根据业务需求修改即可

ps2:该项目使用了 reactHooks dedux ts @根目录配置等。具体配置可以看 我的这篇文章 快速搭建react18.2+TS框架(包含redux,sass,@根目录等有用配置)

废话不说,上代码

tsx:

import React, {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import styles from "./index.module.scss";
import { useDispatch, useSelector } from "react-redux";
import { A2_APIgetGoodsList, A2_APIgetSelectData } from "@/store/action/A2Main";
import { A2GoodsType, A2getGoodsDataType } from "@/types";
import { RootState } from "@/store";
// 这里使用了自己封装的图片懒加载组件,可以自行替换
import ImageLazy from "@/ImageLazy";
import { Input, Select } from "antd";
import classNames from "classnames";
import GoodsInfo from "../GoodsInfo";
import { CloseOutlined } from "@ant-design/icons";
import curImg from "@/assets/img/goods/cur.png";
import curAcImg from "@/assets/img/goods/curAc.png";

function GoodsSw() {
  // 滚轮提示
  const [curTit, setCurTit] = useState(true);

  const curTimeRef = useRef(0);
  const [curCut, setCurCut] = useState(true);

  useEffect(() => {
    if (!curTit) {
      localStorage.setItem("YPZZ_CUR", "1");
      clearInterval(curTimeRef.current);
    }
  }, [curTit]);

  useEffect(() => {
    // 开启滚轮提示 图片更换 定时器
    clearInterval(curTimeRef.current);
    curTimeRef.current = window.setInterval(() => {
      setCurCut(!curCut);
    }, 2000);
  }, [curCut]);

  useEffect(() => {
    const isFlag = localStorage.getItem("YPZZ_CUR");
    if (isFlag) setCurTit(false);
  }, []);

  const dispatch = useDispatch();

  useEffect(() => {
    // 发送请求拿到下拉框数据
    dispatch(A2_APIgetSelectData("age"));
    dispatch(A2_APIgetSelectData("texture"));
  }, [dispatch]);

  // 获取下拉数据
  const selectData = useSelector((state: RootState) => state.A2Main.selectData);

  // 第一次不显示暂无信息
  const [flagOne, setFlagOne] = useState(false);

  useEffect(() => {
    window.setTimeout(() => {
      setFlagOne(true);
    }, 500);
  }, []);

  // 发送请求函数
  const [getData, setGetData] = useState<A2getGoodsDataType>({
    dictAge: "",
    dictTexture: "",
    searchKey: "",
    pageNum: 1,
    pageSize: 9999,
  });

  const getListFu = useCallback(async () => {
    await dispatch(A2_APIgetGoodsList(getData));
  }, [dispatch, getData]);

  // 从仓库获取数据
  const goodsListAll = useSelector(
    (state: RootState) => state.A2Main.goodsList
  );

  // 分为上下2个模块
  const list1 = useMemo(() => {
    const arr = [] as A2GoodsType[];
    goodsListAll.forEach((v, i) => {
      if (i % 2 === 0) arr.push(v);
    });
    if (arr.length > 4) {
      for (let i = 0; i < 4; i++) {
        arr.push(arr[i]);
      }
    }
    return arr;
  }, [goodsListAll]);

  // 下面的数组
  const list2 = useMemo(() => {
    const arr = [] as A2GoodsType[];
    goodsListAll.forEach((v, i) => {
      if (i % 2 !== 0) arr.push(v);
    });
    if (arr.length > 4) {
      for (let i = 0; i < 4; i++) {
        arr.push(arr[i]);
      }
    }
    return arr;
  }, [goodsListAll]);

  useEffect(() => {
    // 发送请求
    getListFu();
  }, [getListFu]);

  // 名称的输入
  const nameTime = useRef(-1);
  const nameChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      clearTimeout(nameTime.current);
      nameTime.current = window.setTimeout(() => {
        setGetData({
          ...getData,
          searchKey: e.target.value,
        });
      }, 500);
    },
    [getData]
  );

  // -----点击了单个藏品打开的页面----------
  const [open, setOpen] = useState(false);
  const openRef = useRef(0);

  // 第一个盒子的总宽度
  const goddsSw1 = useMemo(() => {
    return window.innerWidth * 0.25 * list1.length;
  }, [list1.length]);

  // 4个定时器
  const timeS1 = useRef(-1);
  const timeS2 = useRef(-1);
  const time1 = useRef(-1);
  const time2 = useRef(-1);

  // 上面的开始自动滚动
  const scroolFu1 = useCallback(
    (dom: HTMLDivElement, num: number, time: number) => {
      clearTimeout(timeS1.current);
      timeS1.current = window.setTimeout(() => {
        clearInterval(time1.current);
        let numAuto = num;
        time1.current = window.setInterval(() => {
          numAuto += 1;

          if (dom.scrollLeft + dom.clientWidth >= dom.scrollWidth) {
            numAuto = 0;
          }
          dom.scrollLeft = numAuto;
        }, 20);
      }, time);
    },
    []
  );

  // 清理第一个定时器
  const colseTime1 = useCallback(() => {
    clearTimeout(timeS1.current);
    clearInterval(time1.current);
    const dom = document.querySelector("#goddsSwBox1") as HTMLDivElement;
    if (dom && list1.length > 4 && !open) {
      scroolFu1(dom, dom.scrollLeft, 3000);
    }
  }, [list1, open, scroolFu1]);

  // 下面的开始自动滚动
  const scroolFu2 = useCallback(
    (dom: HTMLDivElement, num: number, time: number) => {
      clearTimeout(timeS2.current);
      timeS2.current = window.setTimeout(() => {
        clearInterval(time2.current);
        let numAuto = num;
        dom.scrollLeft = numAuto;
        time2.current = window.setInterval(() => {
          numAuto -= 1;
          if (numAuto <= 0) {
            numAuto = dom.scrollWidth - dom.clientWidth;
          }
          dom.scrollLeft = numAuto;
        }, 20);
      }, time);
    },
    []
  );

  // 清理第二个定时器
  const closeTime2 = useCallback(() => {
    clearTimeout(timeS2.current);
    clearInterval(time2.current);
    const dom = document.querySelector("#goddsSwBox2") as HTMLDivElement;
    if (dom && list2.length > 4 && !open) {
      scroolFu2(dom, dom.scrollLeft, 3000);
    }
  }, [list2, open, scroolFu2]);

  // 上面的数组改变了数据
  useEffect(() => {
    if (list1.length > 4) {
      const dom = document.querySelector("#goddsSwBox1") as HTMLDivElement;
      // 监听鼠标滚轮
      dom.onwheel = (e) => {
        // 使用过滚轮
        setCurTit(false);

        e.preventDefault();
        const num = dom.scrollLeft;
        if (dom.scrollLeft + dom.clientWidth >= dom.scrollWidth)
          dom.scrollLeft = 0;
        // else if (num <= 0) dom.scrollLeft = dom.scrollWidth - dom.clientWidth;
        else dom.scrollLeft = num + e.deltaY;
        colseTime1();
      };
      // 开启定时器
      scroolFu1(dom, 0, 1000);
    } else colseTime1();
  }, [colseTime1, list1, scroolFu1]);

  // 下面的数组改变了数据
  useEffect(() => {
    if (list2.length > 4) {
      const dom = document.querySelector("#goddsSwBox2") as HTMLDivElement;
      // 初始滚动到末尾
      dom.scrollLeft = dom.scrollWidth - dom.clientWidth;
      // 监听鼠标滚轮
      dom.onwheel = (e) => {
        // 使用过滚轮
        setCurTit(false);

        e.preventDefault();
        const num = dom.scrollLeft;
        if (num <= 0) dom.scrollLeft = dom.scrollWidth - dom.clientWidth;
        // else if (dom.scrollLeft + dom.clientWidth >= dom.scrollWidth)
        //   dom.scrollLeft = 0;
        else dom.scrollLeft = num + e.deltaY;

        closeTime2();
      };
      // 开启定时器
      scroolFu2(dom, dom.scrollWidth - dom.clientWidth, 1000);
    } else closeTime2();
  }, [closeTime2, list2, scroolFu2]);

  // 第二个盒子的总宽度
  const goddsSw2 = useMemo(() => {
    return window.innerWidth * 0.25 * list2.length;
  }, [list2.length]);

  // 点击藏品
  const clickGoodFu = useCallback((id: number) => {
    openRef.current = id;

    setOpen(true);

    // console.log(123, id);
  }, []);

  // 打开了单个文物详情页,关闭之后的逻辑
  useEffect(() => {
    if (open) {
      clearTimeout(timeS1.current);
      clearInterval(time1.current);
      clearTimeout(timeS2.current);
      clearInterval(time2.current);
    } else if (openRef.current) {
      const dom1 = document.querySelector("#goddsSwBox1") as HTMLDivElement;
      const dom2 = document.querySelector("#goddsSwBox2") as HTMLDivElement;
      scroolFu1(dom1, dom1.scrollLeft, 3000);
      scroolFu2(dom2, dom2.scrollLeft, 3000);
    }
  }, [open, scroolFu1, scroolFu2]);

  // 离开页面的时候清理4个定时器
  useEffect(() => {
    return () => {
      clearTimeout(timeS1.current);
      clearTimeout(timeS2.current);
      clearInterval(time1.current);
      clearInterval(time2.current);
      // 清理 滚轮提示 定时器
      clearInterval(curTimeRef.current);
    };
  }, []);

  return (
    <div className={styles.GoodsSw}>
      {/* 轮播图主体 */}

      {goodsListAll.length <= 0 ? (
        <div className="noInfo" hidden={!flagOne}>
          暂无更多内容
        </div>
      ) : (
        <div className="goddsBox">
          {/* 第一个自动播发的盒子 */}
          <div
            id="goddsSwBox1"
            className="goddsSwBox"
            onClick={() => colseTime1()}
            // onMouseLeave={() => auto1Fu()}
          >
            <div className="goddsSw" style={{ width: goddsSw1 + "px" }}>
              {list1.map((v, i) => (
                <div key={i} className="goddsRow">
                  <div className="name">
                    {v.name.length > 10
                      ? v.name.substring(0, 10) + "..."
                      : v.name}
                  </div>
                  <div
                    title={v.name}
                    className="goddsImg"
                    onClick={() => clickGoodFu(v.id)}
                  >
                    <ImageLazy
                      src={v.thumb}
                      width="100%"
                      height="100%"
                      noLook
                    />
                  </div>
                </div>
              ))}
            </div>
          </div>
          {/* 第二个自动播放的盒子 */}
          <div
            id="goddsSwBox2"
            className="goddsSwBox goddsSwBox2"
            onClick={() => closeTime2()}
            // onMouseLeave={() => auto2Fu()}
          >
            <div className="goddsSw" style={{ width: goddsSw2 + "px" }}>
              {list2.map((v, i) => (
                <div key={i} className="goddsRow" title={v.name}>
                  <div className="name">
                    {v.name.length > 10
                      ? v.name.substring(0, 10) + "..."
                      : v.name}
                  </div>
                  <div className="goddsImg" onClick={() => clickGoodFu(v.id)}>
                    <ImageLazy
                      src={v.thumb}
                      width="100%"
                      height="100%"
                      noLook
                    />
                  </div>
                </div>
              ))}
            </div>
          </div>
        </div>
      )}

      {/* 下面的筛选框 */}
      <div className="searchBox">
        <div className="searRow">
          <Select
            placeholder="种类"
            style={{ width: 150 }}
            options={selectData["texture"]}
            onChange={(e) => setGetData({ ...getData, dictTexture: e })}
          />
        </div>
        <div className="searRow">
          <Select
            placeholder="年代"
            style={{ width: 150 }}
            options={selectData["age"]}
            onChange={(e) => setGetData({ ...getData, dictAge: e })}
          />
        </div>
        <div className="searRow">
          <Input
            maxLength={25}
            style={{ width: 220 }}
            placeholder="请输入关键词"
            allowClear
            onChange={(e) => nameChange(e)}
          />
        </div>
      </div>

      {/* 单个藏品详情盒子 */}
      {/* ***********这部分代码和页面自行准备 和 处理*********** */}
      <div className={classNames("goodsInfoBox", open ? "goodsInfoBoxAc" : "")}>
        {open ? (
          <GoodsInfo
            isOpen={false}
            id={openRef.current}
            closePage={() => setOpen(false)}
          />
        ) : null}
      </div>

      {/* 滚轮提示 */}
      {curTit ? (
        <div className="curTitBox" hidden={open}>
          <div className="curClose" onClick={() => setCurTit(false)}>
            <CloseOutlined />
          </div>
          <div className="curImg">
            <img src={curImg} alt="" style={{ opacity: curCut ? 1 : 0 }} />
            <img src={curAcImg} alt="" style={{ opacity: curCut ? 0 : 1 }} />
          </div>
          <p>鼠标滚轮可控制图片滚动</p>
        </div>
      ) : null}
    </div>
  );
}

const MemoGoodsSw = React.memo(GoodsSw);

export default MemoGoodsSw;

scss:

.GoodsSw {
  height: calc(100% - 100px);

  :global {
    .noInfo {
      height: calc(100% - 100px);
      display: flex;
      justify-content: center;
      align-items: center;
      color: var(--themeColor);
      font-size: 40px;
      padding-top: 100px;
    }

    .goddsBox {
      height: calc(100% - 100px);




      // 第一个自动播发的盒子
      .goddsSwBox {
        height: calc(50% - 20px);
        border-bottom: 1px solid rgba(231, 219, 188, 0.50);
        position: relative;
        width: 100%;
        overflow-x: auto;

        &::-webkit-scrollbar {
          /*滚动条整体样式*/
          width: 0px;
          /*高宽分别对应横竖滚动条的尺寸*/
          height: 0px;
        }



        .goddsSw {
          display: flex;
          // position: absolute;
          // left: 0;
          // top: 0;
          height: 100%;

          .goddsRow {
            width: 25vw;
            color: #fff;
            padding: 0 10px 0 90px;
            display: flex;
            height: 100%;

            .name {
              width: 40px;
              margin-right: 20px;
              word-wrap: break-word;
              writing-mode: vertical-lr;
              writing-mode: tb-lr;
              font-size: 16px;
              color: #C8B992;
              padding-top: 20px;
              letter-spacing: 3px;
              position: relative;

              &::before {
                content: '';
                position: absolute;
                top: 0;
                left: 7.5px;
                width: 8px;
                height: 8px;
                border-radius: 50%;
                background-color: #C8B992;
              }
            }

            .goddsImg {
              cursor: pointer;

              width: calc(100% - 80px);
              height: calc(100% - 80px);
              position: relative;

              &::before {
                pointer-events: none;
                content: '';
                position: absolute;
                bottom: -80px;
                left: 50%;
                transform: translateX(-50%);
                background: linear-gradient(rgba(231, 219, 188, 0), rgba(231, 219, 188, 1));
                width: 1px;
                height: 90px;
              }

              img {
                pointer-events: none;
                object-fit: contain;
              }
            }
          }
        }


      }



      .goddsSwBox2 {
        overflow-y: hidden;
        border-bottom: none;
        border-top: 1px solid rgba(231, 219, 188, 0.50);

        margin-top: 40px;

        .goddsSw {

          .goddsRow {
            padding: 0 90px 0 10px;

            .name {
              position: relative;
              top: 80px;
            }

            .goddsImg {
              top: 80px;

              &::before {
                bottom: auto;
                top: -80px;
                background: linear-gradient(rgba(231, 219, 188, 1), rgba(231, 219, 188, 0));
              }

            }
          }
        }
      }


    }

    .searchBox {
      height: 100px;
      display: flex;
      align-items: center;
      justify-content: center;

      .searRow {
        margin: 20px;



        .ant-select-selector {
          border-radius: 0 6px 0px 6px;
          background-color: transparent;
          color: var(--themeColor);
          border: 1px solid var(--themeColor);

          .ant-select-selection-search-input::-webkit-input-placeholder {
            /* WebKit browsers */
            color: var(--themeColor);
          }

          .ant-select-selection-search-input:-moz-placeholder {
            /* Mozilla Firefox 4 to 18 */
            color: var(--themeColor);
          }

          .ant-select-selection-search-input::-moz-placeholder {
            /* Mozilla Firefox 19+ */
            color: var(--themeColor);
          }

          .ant-select-selection-search-input:-ms-input-placeholder {
            /* Internet Explorer 10+ */
            color: var(--themeColor);
          }

          .ant-select-selection-item {
            color: var(--themeColor);
          }

          .ant-select-selection-placeholder {
            color: var(--themeColor);
          }

        }

        .ant-select-arrow {
          color: var(--themeColor);
        }

        .anticon-down {
          color: var(--themeColor);
        }
      }

      .ant-input-affix-wrapper {
        border-radius: 0 6px 0px 6px;
        background-color: transparent;
        color: var(--themeColor);
        border: 1px solid var(--themeColor);

        .ant-input {
          background-color: transparent;
          color: var(--themeColor);

          &::-webkit-input-placeholder {
            /* WebKit browsers */
            color: var(--themeColor);
          }

          &:-moz-placeholder {
            /* Mozilla Firefox 4 to 18 */
            color: var(--themeColor);
          }

          &::-moz-placeholder {
            /* Mozilla Firefox 19+ */
            color: var(--themeColor);
          }

          &:-ms-input-placeholder {
            /* Internet Explorer 10+ */
            color: var(--themeColor);
          }
        }

        .ant-input-clear-icon {
          color: var(--themeColor);
        }
      }

    }

    // 藏品详情
    .goodsInfoBox {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      opacity: 0;
      pointer-events: none;
      transition: all .3s;
      backdrop-filter: blur(10px);
      padding-top: 30px;

      &>div {
        color: #fff;
      }
    }

    .goodsInfoBoxAc {
      opacity: 1;
      pointer-events: auto;
    }

    .curTitBox {
      border-radius: 0 0 4px 4px;
      border-top: 3px solid #b6ab97;
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      width: 300px;
      height: 370px;
      background-color: #726b56;

      .curClose {
        position: absolute;
        right: 15px;
        top: 15px;
        cursor: pointer;
        color: #98856f;
        font-size: 24px;
      }

      .curImg {
        margin: 70px auto 30px;
        width: 213px;
        height: 213px;
        position: relative;

        &>img {
          position: absolute;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          opacity: 1;
          transition: all 2s;
        }
      }

      &>p {
        font-size: 14px;
        color: #fff;
        text-align: center;
      }
    }
  }
}