react js循环滚动

javascript 复制代码
import { regTotalByUnits } from '@/pages/dataJudge/service';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import './letterCountRank.less';
import ModuleTitle from './moduleTitle';
import { AreaBreadCrumb } from '../module/mapJson/getJson';

interface ILetterCountRankItem {
  orgName: string;
  num: string | number;
}

const ROW_HEIGHT = 56;
const SCROLL_SPEED = 24; // px/s

const LetterCountRank = ({ date, breadCrumb }: { date: string[] | null; breadCrumb?: AreaBreadCrumb[] }) => {
  const viewportRef = useRef<HTMLDivElement | null>(null);
  const animationFrameRef = useRef<number | null>(null);
  const lastTimestampRef = useRef<number | null>(null);
  const offsetRef = useRef<number>(0);
  const pausedRef = useRef(false);
  const [dataInfo, setDataInfo] = useState<ILetterCountRankItem[]>([]);
  const [offsetY, setOffsetY] = useState(0);
  const [containerHeight, setContainerHeight] = useState(0);
  const [currentArea, setCurrentArea] = useState('省级各地市');

  const rankData = useMemo(() => {
    return [...dataInfo]
      .map((item) => ({
        ...item,
        num: Number(item?.num || 0),
      }))
      .sort((a, b) => Number(b.num) - Number(a.num));
  }, [dataInfo]);

  const maxValue = useMemo(() => {
    return Math.max(...rankData.map((item) => Number(item.num)), 0);
  }, [rankData]);

  const visibleCount = useMemo(() => {
    if (!containerHeight) {
      return 0;
    }
    return Math.max(1, Math.floor(containerHeight / ROW_HEIGHT));
  }, [containerHeight]);

  const shouldScroll = useMemo(() => {
    return rankData.length > visibleCount && visibleCount > 0;
  }, [rankData.length, visibleCount]);

  const displayList = useMemo(() => {
    return shouldScroll ? [...rankData, ...rankData] : rankData;
  }, [rankData, shouldScroll]);

  const updateContainerHeight = useCallback(() => {
    const nextHeight = viewportRef.current?.clientHeight || 0;
    setContainerHeight((prev) => (prev === nextHeight ? prev : nextHeight));
  }, []);

  const cancelAnimation = useCallback(() => {
    if (animationFrameRef.current !== null) {
      cancelAnimationFrame(animationFrameRef.current);
      animationFrameRef.current = null;
    }
    lastTimestampRef.current = null;
  }, []);

  const startAutoScroll = useCallback(() => {
    cancelAnimation();
    if (!shouldScroll) {
      offsetRef.current = 0;
      setOffsetY(0);
      return;
    }

    const singleListHeight = rankData.length * ROW_HEIGHT;

    const animate = (timestamp: number) => {
      if (pausedRef.current) {
        lastTimestampRef.current = timestamp;
        animationFrameRef.current = requestAnimationFrame(animate);
        return;
      }

      const delta = lastTimestampRef.current ? timestamp - lastTimestampRef.current : 16;
      lastTimestampRef.current = timestamp;
      offsetRef.current += (SCROLL_SPEED * delta) / 1000;

      if (offsetRef.current >= singleListHeight) {
        offsetRef.current = 0;
      }

      setOffsetY(offsetRef.current);
      animationFrameRef.current = requestAnimationFrame(animate);
    };

    animationFrameRef.current = requestAnimationFrame(animate);
  }, [cancelAnimation, rankData.length, shouldScroll]);

  const handleWheel = useCallback(
    (e: WheelEvent) => {
      if (!shouldScroll) return;
      e.preventDefault();
      const singleListHeight = rankData.length * ROW_HEIGHT;
      const maxOffset = Math.max(0, singleListHeight - containerHeight);
      offsetRef.current = Math.max(0, Math.min(offsetRef.current + e.deltaY, maxOffset));
      setOffsetY(offsetRef.current);
    },
    [shouldScroll, rankData.length, containerHeight],
  );

  useEffect(() => {
    const areaInfo = breadCrumb[breadCrumb.length - 1];
    setCurrentArea(areaInfo.name === '河南省' ? '省级各地市' : `${ areaInfo.name } 市级各区县`);
    const dateObj: any = date && date.length !== 0 ? {
      startTime: date[0], endTime: date[1],
      city: areaInfo.name
     } : {};
    regTotalByUnits({ ...dateObj })
      .then((res) => {
        if (res.success) {
          setDataInfo(res.result || []);
        } else {
          setDataInfo([]);
        }
      })
      .catch((error) => {
        setDataInfo([]);
        return error;
      });
  }, [date, breadCrumb]);

  useEffect(() => {
    startAutoScroll();
  }, [startAutoScroll]);

  useEffect(() => {
    const handleResize = () => {
      updateContainerHeight();
    };

    updateContainerHeight();
    window.addEventListener('resize', handleResize);
    return () => {
      cancelAnimation();
      window.removeEventListener('resize', handleResize);
    };
  }, [cancelAnimation, updateContainerHeight]);

  useEffect(() => {
    const viewport = viewportRef.current;
    if (!viewport) return;
    viewport.addEventListener('wheel', handleWheel, { passive: false });
    return () => {
      viewport.removeEventListener('wheel', handleWheel);
    };
  }, [handleWheel]);

  useEffect(() => {
    const frameId = requestAnimationFrame(() => {
      updateContainerHeight();
    });
    offsetRef.current = 0;
    setOffsetY(0);
    return () => {
      cancelAnimationFrame(frameId);
    };
  }, [rankData, updateContainerHeight]);
  // 如果是 currentArea 
  
  return (
    <ModuleTitle title="省级各地市信访件排名情况">
      <div
        className="letterCountRank"
        onMouseEnter={() => {
          pausedRef.current = true;
          cancelAnimation();
        }}
        onMouseLeave={() => {
          pausedRef.current = false;
          startAutoScroll();
        }}
      >
        <div className="letterCountRank__header">
          <span className="letterCountRank__headerRank">排名</span>
          <span className="letterCountRank__headerName">单位名称</span>
          <span className="letterCountRank__headerValue">信访数量</span>
        </div>
        {!rankData.length ? (
          <div className="letterCountRank__empty">暂无数据</div>
        ) : (
          <div ref={viewportRef} className="letterCountRank__viewport">
            <div
              className="letterCountRank__track"
              style={{ transform: `translate3d(0, -${offsetY}px, 0)` }}
            >
              {displayList.map((item, index) => {
                const realIndex = index % rankData.length;
                const rank = realIndex + 1;
                const value = Number(item.num);
                const percent = maxValue > 0 ? Math.max((value / maxValue) * 100, 8) : 0;
                return (
                  <div
                    key={`${item.orgName}-${index}`}
                    className="letterCountRank__item"
                    style={{ height: ROW_HEIGHT }}
                  >
                    <div className={`letterCountRank__badge letterCountRank__badge--${rank}`}>
                      {rank}
                    </div>
                    <div className="letterCountRank__content">
                      <div className="letterCountRank__name" title={item.orgName}>
                        {item.orgName}
                      </div>
                      <div className="letterCountRank__barWrap">
                        <div className="letterCountRank__barBg">
                          <div
                            className="letterCountRank__barFill"
                            style={{ width: `${percent}%` }}
                          />
                        </div>
                        <span className="letterCountRank__value">{value}</span>
                      </div>
                    </div>
                  </div>
                );
              })}
            </div>
          </div>
        )}
      </div>
    </ModuleTitle>
  );
};

export default LetterCountRank;
相关推荐
玉宇夕落2 小时前
详细流式输出学习
前端
秃头披风侠_郑2 小时前
【uniapp】一文让你学会微信小程序+APP+H5全平台实战指南
前端·微信小程序·uni-app
嘟嘟07172 小时前
从零搞懂 RAG 文档处理:逐行拆解 Loader 和 Splitter,一行代码背后发生了什么
前端
come112342 小时前
Vue 3 与现代 JavaScript 常用语法指南
前端·javascript·vue.js
dsyyyyy11012 小时前
用JavaScript实现排序算法
开发语言·javascript·排序算法
槑有老呆2 小时前
从零搭建 LLM 流式对话前端:一文读懂 SSE、二进制流与 Vue 3 响应式
前端·javascript
没落英雄2 小时前
7. 从零开始搭建一个 AI Agent —— UI 卡片渲染系统
前端·人工智能·架构
前端缘梦2 小时前
LangGraph 核心特性技术详解:流式输出、持久化、记忆体系与中断实战。
前端·人工智能·程序员
张龙6872 小时前
前端错误监控生产级方案:从 onerror 捕获到 Source Map 精准还原
前端·javascript