feat: 实现 Socket 触发的智能列表自动刷新功能(带防抖)

核心改动:
- 扩展 NotificationContext,添加事件更新回调注册机制
- VirtualizedFourRowGrid 添加 forwardRef 暴露 getScrollPosition 方法
- DynamicNewsCard 实现智能刷新逻辑(根据模式和滚动位置判断是否刷新)
- Community 页面注册 Socket 回调自动触发刷新
- 创建 TypeScript 通用防抖工具函数(debounce.ts)
- 集成防抖机制(2秒延迟),避免短时间内频繁请求

智能刷新策略:
- 纵向模式 + 第1页:自动刷新列表
- 纵向模式 + 其他页:不刷新(避免打断用户)
- 平铺模式 + 滚动在顶部:自动刷新列表
- 平铺模式 + 滚动不在顶部:仅显示 Toast 提示

防抖效果:
- 短时间内收到多个新事件,只执行最后一次刷新
- 减少服务器压力,提升用户体验

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-11-14 19:04:00 +08:00
parent 9fd618c087
commit ddd6b2d4af
7 changed files with 382 additions and 9 deletions

View File

@@ -1,7 +1,7 @@
// src/views/Community/components/DynamicNewsCard.js
// 横向滚动事件卡片组件(实时要闻·动态追踪)
import React, { forwardRef, useState, useEffect, useMemo, useCallback, useRef } from 'react';
import React, { forwardRef, useState, useEffect, useMemo, useCallback, useRef, useImperativeHandle } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
Card,
@@ -44,8 +44,9 @@ import {
selectFourRowEventsWithLoading
} from '../../../store/slices/communityDataSlice';
import { usePagination } from './DynamicNewsCard/hooks/usePagination';
import { PAGINATION_CONFIG, DISPLAY_MODES } from './DynamicNewsCard/constants';
import { PAGINATION_CONFIG, DISPLAY_MODES, REFRESH_DEBOUNCE_DELAY } from './DynamicNewsCard/constants';
import { PROFESSIONAL_COLORS } from '../../../constants/professionalTheme';
import { debounce } from '../../../utils/debounce';
// 🔍 调试:渲染计数器
let dynamicNewsCardRenderCount = 0;
@@ -84,6 +85,7 @@ const DynamicNewsCard = forwardRef(({
// Refs
const cardHeaderRef = useRef(null);
const cardBodyRef = useRef(null);
const virtualizedGridRef = useRef(null); // ⚡ VirtualizedFourRowGrid 的 ref用于获取滚动位置
// 从 Redux 读取关注状态
const eventFollowStatus = useSelector(selectEventFollowStatus);
@@ -208,6 +210,124 @@ const [currentMode, setCurrentMode] = useState('vertical');
setCurrentMode(mode);
}, [mode]);
/**
* ⚡【核心逻辑】执行刷新的回调函数(包含原有的智能刷新逻辑)
*
* 此函数会被 debounce 包装,避免短时间内频繁刷新
*/
const executeRefresh = useCallback(() => {
const state = {
mode,
currentPage: pagination?.current_page || 1,
};
console.log('[DynamicNewsCard] ⏰ executeRefresh() 执行(防抖延迟后)', state);
if (mode === 'vertical') {
// ========== 纵向模式 ==========
// 只在第1页时刷新避免打断用户浏览其他页
if (state.currentPage === 1) {
console.log('[DynamicNewsCard] 纵向模式 + 第1页 → 刷新列表');
handlePageChange(1); // 清空缓存并刷新第1页
toast({
title: '检测到新事件',
status: 'info',
duration: 2000,
isClosable: true,
});
} else {
console.log(`[DynamicNewsCard] 纵向模式 + 第${state.currentPage}页 → 不刷新(避免打断用户)`);
}
} else if (mode === 'four-row') {
// ========== 平铺模式 ==========
// 检查滚动位置,只有在顶部时才刷新
const scrollPos = virtualizedGridRef.current?.getScrollPosition();
if (scrollPos?.isNearTop) {
// 用户在顶部 10% 区域,安全刷新
console.log('[DynamicNewsCard] 平铺模式 + 滚动在顶部 → 刷新列表');
handlePageChange(1); // 清空并刷新
toast({
title: '检测到新事件,已刷新',
status: 'info',
duration: 2000,
isClosable: true,
});
} else {
// 用户不在顶部,显示提示但不自动刷新
console.log('[DynamicNewsCard] 平铺模式 + 滚动不在顶部 → 仅提示,不刷新');
toast({
title: '有新事件发布',
description: '滚动到顶部查看',
status: 'info',
duration: 3000,
isClosable: true,
});
}
}
}, [mode, pagination, handlePageChange, toast]);
/**
* ⚡【防抖包装】创建防抖版本的刷新函数
*
* 使用 useMemo 确保防抖函数在 executeRefresh 不变时保持引用稳定
* 防抖延迟REFRESH_DEBOUNCE_DELAY (2000ms)
*
* 效果:短时间内收到多个新事件,只执行最后一次刷新
*/
const debouncedRefresh = useMemo(
() => debounce(executeRefresh, REFRESH_DEBOUNCE_DELAY),
[executeRefresh]
);
/**
* ⚡ 暴露方法给父组件(用于 Socket 自动刷新)
*/
useImperativeHandle(ref, () => ({
/**
* 智能刷新方法(带防抖,避免频繁刷新)
*
* 调用此方法时:
* 1. 清除之前的定时器(如果有)
* 2. 设置新的定时器(延迟 REFRESH_DEBOUNCE_DELAY 后执行)
* 3. 如果在延迟期间再次调用,重复步骤 1-2
* 4. 只有最后一次调用会在延迟后实际执行 executeRefresh()
*/
refresh: () => {
console.log('[DynamicNewsCard] 🔔 refresh() 被调用(设置防抖定时器)', {
mode,
currentPage: pagination?.current_page || 1,
debounceDelay: `${REFRESH_DEBOUNCE_DELAY}ms`,
});
// 调用防抖包装后的函数
debouncedRefresh();
},
/**
* 获取当前状态(用于调试)
*/
getState: () => ({
mode,
currentPage: pagination?.current_page || 1,
totalPages: pagination?.total_pages || 1,
total: pagination?.total || 0,
loading,
}),
}), [mode, pagination, loading, debouncedRefresh]);
/**
* ⚡【清理逻辑】组件卸载时取消待执行的防抖函数
*
* 作用:避免组件卸载后仍然执行刷新操作(防止内存泄漏和潜在错误)
*/
useEffect(() => {
return () => {
console.log('[DynamicNewsCard] 🧹 组件卸载,取消待执行的防抖刷新');
debouncedRefresh.cancel();
};
}, [debouncedRefresh]);
// 监听 error 状态,显示空数据提示
useEffect(() => {
if (error && error.includes('暂无更多数据')) {
@@ -578,6 +698,7 @@ const [currentMode, setCurrentMode] = useState('vertical');
eventFollowStatus={eventFollowStatus}
onToggleFollow={handleToggleFollow}
hasMore={hasMore}
virtualizedGridRef={virtualizedGridRef} // ⚡ 传递 ref 给 VirtualizedFourRowGrid
/>
</Box>
</CardBody>