Merge pull request #202 from yokowu/feat-activity-level

feat: 优化热力图组件展示
This commit is contained in:
Yoko
2025-08-04 19:03:17 +08:00
committed by GitHub

View File

@@ -1,10 +1,9 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState, useCallback } from 'react';
import Card from '@/components/card';
import dayjs from 'dayjs';
import {
Box,
Stack,
Typography,
Tooltip,
useTheme,
IconButton,
@@ -21,7 +20,6 @@ const getRecent1YearData = (
data: DomainUserHeatmap[] = [],
max_count: number
) => {
const average = max_count / 5;
const today = dayjs();
const lastYearToday = today.subtract(1, 'year');
const diffInDays = today.diff(lastYearToday, 'day');
@@ -31,21 +29,133 @@ const getRecent1YearData = (
dateMap[dayjs.unix(item.date!).format('YYYY-MM-DD')] = item.count!;
});
const getLevel = (count: number) => {
if (count === 0) return 0;
if (count === 1) return 1;
if (count <= Math.max(2, max_count * 0.25)) return 2;
if (count <= Math.max(3, max_count * 0.6)) return 3;
return 4;
};
for (let i = 0; i < diffInDays; i++) {
const time = today.subtract(i, 'day').format('YYYY-MM-DD');
if (dateMap[time]) {
result.unshift({
count: dateMap[time],
date: time,
level: Math.ceil(dateMap[time] / average) - 1,
});
} else {
result.unshift({ count: 0, date: time, level: 0 });
}
const count = dateMap[time] || 0;
result.unshift({
count,
date: time,
level: getLevel(count),
});
}
return result;
};
// 自定义Hook处理ActivityCalendar自动滚动
const useActivityCalendarAutoScroll = () => {
const scrollToLatest = useCallback(() => {
// 尝试多种可能的选择器策略
const selectors = [
'.react-activity-calendar__scroll-container',
'.react-activity-calendar [style*="overflow"]',
'.react-activity-calendar > div:first-child',
];
let scrollContainer: HTMLElement | null = null;
// 按优先级尝试找到滚动容器
for (const selector of selectors) {
scrollContainer = document.querySelector(selector);
if (scrollContainer && scrollContainer.scrollWidth > scrollContainer.clientWidth) {
break;
}
}
if (scrollContainer) {
// 滚动到最右侧(最新数据)
scrollContainer.scrollLeft = scrollContainer.scrollWidth - scrollContainer.clientWidth;
}
}, []);
const setupAutoScroll = useCallback(() => {
// 延迟执行确保组件完全渲染
const timeoutId = setTimeout(scrollToLatest, 100);
// 使用ResizeObserver监听容器大小变化
let resizeObserver: ResizeObserver | null = null;
const setupResizeObserver = () => {
const container = document.querySelector('.react-activity-calendar');
if (container && 'ResizeObserver' in window) {
resizeObserver = new ResizeObserver(() => {
scrollToLatest();
});
resizeObserver.observe(container);
} else {
// 降级方案使用window resize事件
window.addEventListener('resize', scrollToLatest);
}
};
// 延迟设置ResizeObserver确保DOM已渲染
const observerTimeoutId = setTimeout(setupResizeObserver, 150);
// 清理函数
return () => {
clearTimeout(timeoutId);
clearTimeout(observerTimeoutId);
if (resizeObserver) {
resizeObserver.disconnect();
} else {
window.removeEventListener('resize', scrollToLatest);
}
};
}, [scrollToLatest]);
return { setupAutoScroll };
};
// 简化的blockSize计算Hook - 保持原有大小
const useBlockSize = (containerRef: React.RefObject<HTMLElement>) => {
const [blockSize, setBlockSize] = useState(8);
useEffect(() => {
const calculateBlockSize = () => {
if (!containerRef.current) return;
const containerWidth = containerRef.current.offsetWidth;
const baseWidth = 980;
const blockIncrement = 54;
// 只在桌面端进行计算,保持原有逻辑
const increment = Math.max(0, Math.ceil((containerWidth - baseWidth) / blockIncrement));
setBlockSize(increment + 8);
};
// 初始计算
calculateBlockSize();
// 使用ResizeObserver监听容器大小变化
let resizeObserver: ResizeObserver | null = null;
if ('ResizeObserver' in window && containerRef.current) {
resizeObserver = new ResizeObserver(calculateBlockSize);
resizeObserver.observe(containerRef.current);
} else {
// 降级方案
window.addEventListener('resize', calculateBlockSize);
}
return () => {
if (resizeObserver) {
resizeObserver.disconnect();
} else {
window.removeEventListener('resize', calculateBlockSize);
}
};
}, [containerRef]);
return blockSize;
};
const MemberInfo = ({
data,
memberData,
@@ -58,22 +168,27 @@ const MemberInfo = ({
onMemberChange?: (data: DomainUser) => void;
}) => {
const theme = useTheme();
const [blockSize, setBlockSize] = useState(8);
const ref = useRef<HTMLDivElement>(null);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
// 使用自定义Hooks
const blockSize = useBlockSize(ref as React.RefObject<HTMLElement>);
const { setupAutoScroll } = useActivityCalendarAutoScroll();
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
// 设置自动滚动
useEffect(() => {
const innerWidth = ref.current?.offsetWidth;
const dis = Math.max(0, Math.ceil((innerWidth! - 980) / 54));
console.log(dis);
setBlockSize(dis + 8);
}, []);
const cleanup = setupAutoScroll();
return cleanup;
}, [setupAutoScroll, data, memberData]);
return (
<Card
@@ -151,6 +266,8 @@ const MemberInfo = ({
alignItems: 'center',
position: 'relative',
minWidth: 0,
flex: 1,
overflow: 'auto',
}}
>
<ActivityCalendar
@@ -159,7 +276,7 @@ const MemberInfo = ({
blockSize={blockSize}
theme={{
light: [
'#fff',
theme.palette.grey[100],
theme.palette.grey[300],
theme.palette.grey[500],
theme.palette.grey[700],
@@ -188,9 +305,6 @@ const MemberInfo = ({
more: '较多',
},
}}
// hideTotalCount
// hideMonthLabels
// hideColorLegend
renderBlock={(block, activity) => (
<Tooltip
slotProps={{