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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user