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;