refactor: 优化分页存储架构和缓存逻辑...

This commit is contained in:
zdl
2025-11-06 01:20:07 +08:00
parent c5d6247f49
commit a5e001d975
6 changed files with 295 additions and 249 deletions

View File

@@ -82,36 +82,48 @@ const DynamicNewsCard = forwardRef(({
// 🔍 调试:从 Redux 读取数据
console.log('%c[DynamicNewsCard] 从 Redux 读取数据', 'color: #3B82F6; font-weight: bold;', {
currentMode,
'verticalData.data?.length': verticalData.data?.length || 0,
'verticalData.data type': typeof verticalData.data,
'verticalData.data keys': verticalData.data ? Object.keys(verticalData.data) : [],
'verticalData.total': verticalData.total,
'verticalData.cachedCount': verticalData.cachedCount,
'verticalData.cachedPageCount': verticalData.cachedPageCount,
'verticalData.loading': verticalData.loading,
'fourRowData.data?.length': fourRowData.data?.length || 0,
'fourRowData.total': fourRowData.total,
});
// 根据模式选择数据源(添加默认值避免解构失败)
// 根据模式选择数据源
// 纵向模式data 是页码映射 { 1: [...], 2: [...] }
// 平铺模式data 是数组 [...]
const modeData = currentMode === 'four-row' ? fourRowData : verticalData;
const {
data: allCachedEvents = [],
data = currentMode === 'vertical' ? {} : [], // 纵向是对象,平铺是数组
loading = false,
error = null,
total = 0,
cachedCount = 0
} = currentMode === 'four-row' ? fourRowData : verticalData;
pagination, // 分页元数据
total = 0, // 向后兼容
cachedCount = 0,
cachedPageCount = 0
} = modeData;
// 传递给 usePagination 的数据
const allCachedEventsByPage = currentMode === 'vertical' ? data : undefined;
const allCachedEvents = currentMode === 'four-row' ? data : undefined;
// 🔍 调试:选择的数据源
console.log('%c[DynamicNewsCard] 选择的数据源', 'color: #3B82F6; font-weight: bold;', {
mode: currentMode,
'allCachedEvents.length': allCachedEvents.length,
'allCachedEventsByPage': allCachedEventsByPage ? Object.keys(allCachedEventsByPage) : 'undefined',
'allCachedEvents?.length': allCachedEvents?.length,
total,
cachedCount,
cachedPageCount,
loading,
error
});
// 🔍 调试:记录每次渲染
dynamicNewsCardRenderCount++;
console.log(`%c🔍 [DynamicNewsCard] 渲染 #${dynamicNewsCardRenderCount} - mode=${currentMode}, allCachedEvents.length=${allCachedEvents.length}, total=${total}`, 'color: #FF9800; font-weight: bold; font-size: 14px;');
console.log(`%c🔍 [DynamicNewsCard] 渲染 #${dynamicNewsCardRenderCount} - mode=${currentMode}, allCachedEvents.length=${allCachedEvents?.length || 0}, total=${total}`, 'color: #FF9800; font-weight: bold; font-size: 14px;');
// 关注按钮点击处理
const handleToggleFollow = useCallback((eventId) => {
@@ -141,15 +153,16 @@ const DynamicNewsCard = forwardRef(({
totalPages,
hasMore,
currentPageEvents,
displayEvents, // 新增:累积显示的事件列表
isAccumulateMode, // 新增:是否累积模式
displayEvents, // 当前显示的事件列表
handlePageChange,
handleModeToggle,
loadNextPage, // 新增:加载下一页
loadPrevPage // 新增:加载上一页
loadNextPage, // 加载下一页
loadPrevPage // 加载上一页
} = usePagination({
allCachedEvents,
total,
allCachedEventsByPage, // 纵向模式:页码映射
allCachedEvents, // 平铺模式:数组
pagination, // 分页元数据对象
total, // 向后兼容
cachedCount,
dispatch,
toast,
@@ -183,7 +196,11 @@ const DynamicNewsCard = forwardRef(({
// 初始加载 - 只在组件首次挂载且对应模式数据为空时执行
useEffect(() => {
if (!hasInitialized.current && allCachedEvents.length === 0) {
const isDataEmpty = currentMode === 'vertical'
? Object.keys(allCachedEventsByPage || {}).length === 0
: (allCachedEvents?.length || 0) === 0;
if (!hasInitialized.current && isDataEmpty) {
hasInitialized.current = true;
dispatch(fetchDynamicNews({
mode: mode, // 传递当前模式
@@ -194,7 +211,7 @@ const DynamicNewsCard = forwardRef(({
page: PAGINATION_CONFIG.INITIAL_PAGE, // 然后覆盖 page 参数
}));
}
}, [dispatch, allCachedEvents.length, mode, pageSize]); // ✅ 移除 filters 依赖,避免重复触发
}, [dispatch, allCachedEventsByPage, allCachedEvents, currentMode, mode, pageSize]); // ✅ 移除 filters 依赖,避免重复触发
// 监听筛选条件变化 - 清空缓存并重新请求数据
useEffect(() => {
@@ -231,7 +248,11 @@ const DynamicNewsCard = forwardRef(({
// 监听模式切换 - 如果新模式数据为空,请求数据
useEffect(() => {
if (hasInitialized.current && allCachedEvents.length === 0) {
const isDataEmpty = currentMode === 'vertical'
? Object.keys(allCachedEventsByPage || {}).length === 0
: (allCachedEvents?.length || 0) === 0;
if (hasInitialized.current && isDataEmpty) {
console.log(`%c🔄 [模式切换] ${mode} 模式数据为空,开始加载`, 'color: #8B5CF6; font-weight: bold;');
dispatch(fetchDynamicNews({
mode: mode,
@@ -309,11 +330,10 @@ const DynamicNewsCard = forwardRef(({
{currentPageEvents && currentPageEvents.length > 0 ? (
<EventScrollList
events={currentPageEvents}
displayEvents={displayEvents} // 新增:累积显示的事件列表
isAccumulateMode={isAccumulateMode} // 新增:是否累积模式
loadNextPage={loadNextPage} // 新增:加载一页
loadPrevPage={loadPrevPage} // 新增:加载上一页
onFourRowEventClick={handleFourRowEventClick} // 新增:四排模式事件点击
displayEvents={displayEvents} // 累积显示的事件列表(平铺模式)
loadNextPage={loadNextPage} // 加载下一页
loadPrevPage={loadPrevPage} // 加载一页
onFourRowEventClick={handleFourRowEventClick} // 四排模式事件点击
selectedEvent={selectedEvent}
onEventSelect={setSelectedEvent}
borderColor={borderColor}

View File

@@ -33,7 +33,6 @@ import VerticalModeLayout from './VerticalModeLayout';
const EventScrollList = ({
events,
displayEvents, // 累积显示的事件列表(四排模式用)
isAccumulateMode, // 是否累积模式
loadNextPage, // 加载下一页(无限滚动)
loadPrevPage, // 加载上一页(双向无限滚动)
onFourRowEventClick, // 四排模式事件点击回调(打开弹窗)

View File

@@ -14,23 +14,31 @@ import {
/**
* 分页逻辑自定义 Hook
* @param {Object} options - Hook 配置选项
* @param {Array} options.allCachedEvents - 完整缓存事件列表
* @param {number} options.total - 服务端总数量
* @param {Object} options.allCachedEventsByPage - 纵向模式页码映射 { 1: [...], 2: [...] }
* @param {Array} options.allCachedEvents - 平铺模式数组 [...]
* @param {Object} options.pagination - 分页元数据 { total, total_pages, current_page, per_page }
* @param {number} options.total - 【废弃】服务端总数量(向后兼容,建议使用 pagination.total
* @param {number} options.cachedCount - 已缓存数量
* @param {Function} options.dispatch - Redux dispatch 函数
* @param {Function} options.toast - Toast 通知函数
* @param {Object} options.filters - 筛选条件
* @returns {Object} 分页状态和方法
*/
export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, toast, filters = {} }) => {
export const usePagination = ({
allCachedEventsByPage, // 纵向模式:页码映射
allCachedEvents, // 平铺模式:数组
pagination, // 分页元数据对象
total, // 向后兼容
cachedCount,
dispatch,
toast,
filters = {}
}) => {
// 本地状态
const [currentPage, setCurrentPage] = useState(PAGINATION_CONFIG.INITIAL_PAGE);
const [loadingPage, setLoadingPage] = useState(null);
const [mode, setMode] = useState(DEFAULT_MODE);
// 累积显示的事件列表(用于四排模式的无限滚动)
const [accumulatedEvents, setAccumulatedEvents] = useState([]);
// 根据模式决定每页显示数量
const pageSize = (() => {
switch (mode) {
@@ -43,92 +51,55 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
}
})();
// 计算总页数(基于服务端总数据量)
const totalPages = Math.ceil(total / pageSize) || 1;
// 【优化】优先使用后端返回的 total_pages避免前端重复计算
// 向后兼容:如果没有 pagination 对象,则使用 total 计算
const totalPages = pagination?.total_pages || Math.ceil((pagination?.total || total || 0) / pageSize) || 1;
// 检查是否还有更多数据
const hasMore = cachedCount < total;
// 检查是否还有更多数据(使用页码判断,不受去重影响)
const hasMore = currentPage < totalPages;
// 判断是否使用累积模式(四排模式 + 纵向模式)
const isAccumulateMode = mode === DISPLAY_MODES.FOUR_ROW || mode === DISPLAY_MODES.VERTICAL;
// 从缓存中切片获取当前页数据(过滤 null 占位符)
// 从页码映射或数组获取当前页数据
const currentPageEvents = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
return allCachedEvents.slice(startIndex, endIndex).filter(event => event !== null);
}, [allCachedEvents, currentPage, pageSize]);
// 当前显示的事件列表(累积模式 vs 分页模式)
const displayEvents = useMemo(() => {
if (isAccumulateMode) {
// 四排模式:累积显示所有已加载的事件
return accumulatedEvents;
if (mode === DISPLAY_MODES.VERTICAL) {
// 纵向模式:从页码映射获取当前页
return allCachedEventsByPage?.[currentPage] || [];
} else {
// 其他模式:只显示当前页
// 平铺模式:返回全部累积数据
return allCachedEvents || [];
}
}, [mode, allCachedEventsByPage, allCachedEvents, currentPage]);
// 当前显示的事件列表
const displayEvents = useMemo(() => {
if (mode === DISPLAY_MODES.FOUR_ROW) {
// 平铺模式:返回全部累积数据
return allCachedEvents || [];
} else {
// 纵向模式:返回当前页数据
return currentPageEvents;
}
}, [isAccumulateMode, accumulatedEvents, currentPageEvents]);
/**
* 子函数1: 检查目标页缓存状态
* @param {number} targetPage - 目标页码
* @returns {Object} { isTargetPageCached, targetPageInfo }
*/
const checkTargetPageCache = useCallback((targetPage) => {
const targetPageStartIndex = (targetPage - 1) * pageSize;
const targetPageEndIndex = targetPageStartIndex + pageSize;
const targetPageData = allCachedEvents.slice(targetPageStartIndex, targetPageEndIndex);
const validTargetData = targetPageData.filter(e => e !== null);
// 修复:确保 expectedCount 不为负数
// - 当 total = 0 时expectedCount = pageSize强制发起请求
// - 当 total - targetPageStartIndex < 0 时expectedCount = 0
const expectedCount = total === 0 ? pageSize : Math.max(0, Math.min(pageSize, total - targetPageStartIndex));
const isTargetPageCached = validTargetData.length >= expectedCount;
logger.debug('DynamicNewsCard', '目标页缓存检查', {
targetPage,
targetPageStartIndex,
targetPageEndIndex,
targetPageDataLength: targetPageData.length,
validTargetDataLength: validTargetData.length,
expectedCount,
isTargetPageCached
});
return {
isTargetPageCached,
targetPageInfo: {
startIndex: targetPageStartIndex,
endIndex: targetPageEndIndex,
validCount: validTargetData.length,
expectedCount
}
};
}, [allCachedEvents, pageSize, total]);
// 已删除: calculatePreloadRange不再需要预加载
// 已删除: findMissingPages不再需要查找缺失页面
}, [mode, allCachedEvents, currentPageEvents]);
/**
* 加载单个页面数据
* @param {number} targetPage - 目标页码
* @param {boolean} clearCache - 是否清空缓存第1页专用
* @returns {Promise<boolean>} 是否加载成功
*/
const loadPage = useCallback(async (targetPage) => {
const loadPage = useCallback(async (targetPage, clearCache = false) => {
// 显示 loading 状态
setLoadingPage(targetPage);
try {
console.log(`%c🟢 [API请求] 开始加载第${targetPage}页数据`, 'color: #16A34A; font-weight: bold;');
console.log(`%c 请求参数: page=${targetPage}, per_page=${pageSize}, mode=${mode}`, 'color: #16A34A;');
console.log(`%c 请求参数: page=${targetPage}, per_page=${pageSize}, mode=${mode}, clearCache=${clearCache}`, 'color: #16A34A;');
console.log(`%c 筛选条件:`, 'color: #16A34A;', filters);
logger.debug('DynamicNewsCard', '开始加载页面数据', {
targetPage,
pageSize,
mode,
clearCache,
filters
});
@@ -138,7 +109,7 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
page: targetPage,
per_page: pageSize,
pageSize,
clearCache: false,
clearCache,
filters
});
@@ -146,7 +117,7 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
mode: mode, // 传递 mode 参数
per_page: pageSize,
pageSize: pageSize,
clearCache: false,
clearCache: clearCache, // 传递 clearCache 参数
...filters, // 先展开筛选条件
page: targetPage, // 然后覆盖 page 参数(避免被 filters.page 覆盖)
})).unwrap();
@@ -175,108 +146,94 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
} finally {
setLoadingPage(null);
}
}, [dispatch, pageSize, toast]);
}, [dispatch, pageSize, toast, mode, filters]);
// 翻页处理(简化版 - 无预加载
// 翻页处理(第1页强制刷新 + 其他页缓存
const handlePageChange = useCallback(async (newPage) => {
// 边界检查 1: 检查页码范围
if (newPage < 1 || newPage > totalPages) {
console.log(`%c⚠ [翻页] 页码超出范围: ${newPage}`, 'color: #DC2626; font-weight: bold;');
logger.warn('usePagination', '页码超出范围', { newPage, totalPages });
return;
}
// 边界检查 2: 检查是否重复点击
if (newPage === currentPage) {
console.log(`%c⚠ [翻页] 重复点击当前页: ${newPage}`, 'color: #EAB308; font-weight: bold;');
logger.debug('usePagination', '页码未改变', { newPage });
return;
}
// 边界检查 3: 防止竞态条件 - 如果正在加载其他页面,忽略新请求
if (loadingPage !== null) {
console.log(`%c⚠ [翻页] 正在加载第${loadingPage}页,忽略新请求第${newPage}`, 'color: #EAB308; font-weight: bold;');
logger.warn('usePagination', '竞态条件:正在加载中', { loadingPage, newPage });
return;
}
console.log(`%c🔵 [翻页逻辑] handlePageChange 开始`, 'color: #3B82F6; font-weight: bold;');
console.log(`%c 当前页: ${currentPage}, 目标页: ${newPage}, 总页数: ${totalPages}`, 'color: #3B82F6;');
console.log(`%c 每页大小: ${pageSize}, 缓存总数: ${allCachedEvents.length}, 服务端总数: ${total}`, 'color: #3B82F6;');
console.log(`%c 当前页: ${currentPage}, 目标页: ${newPage}, 模式: ${mode}`, 'color: #3B82F6;');
logger.debug('DynamicNewsCard', '开始翻页', {
currentPage,
newPage,
pageSize,
totalPages,
total,
allCachedEventsLength: allCachedEvents.length,
cachedCount
});
// 【核心逻辑】第1页特殊处理强制清空缓存并重新加载
if (newPage === 1) {
console.log(`%c🔄 [第1页] 清空缓存并重新加载`, 'color: #8B5CF6; font-weight: bold;');
logger.info('usePagination', '第1页强制刷新', { mode });
// 检查目标页缓存状态(统一处理,包括第一页)
const { isTargetPageCached, targetPageInfo } = checkTargetPageCache(newPage);
const success = await loadPage(newPage, true); // clearCache = true
console.log(`%c🟡 [缓存检查] 目标页${newPage}缓存状态`, 'color: #EAB308; font-weight: bold;');
console.log(`%c 是否已缓存: ${isTargetPageCached ? '✅ 是' : '❌ 否'}`, `color: ${isTargetPageCached ? '#16A34A' : '#DC2626'};`);
console.log(`%c 索引范围: ${targetPageInfo.startIndex}-${targetPageInfo.endIndex}`, 'color: #EAB308;');
console.log(`%c 实际数量: ${targetPageInfo.validCount}, 期望数量: ${targetPageInfo.expectedCount}`, 'color: #EAB308;');
if (isTargetPageCached) {
// 目标页已缓存,直接切换
console.log(`%c🟡 [缓存] 目标页已缓存,直接切换到第${newPage}`, 'color: #16A34A; font-weight: bold;');
logger.debug('DynamicNewsCard', '目标页已缓存,直接切换', { newPage });
setCurrentPage(newPage);
} else {
// 目标页未缓存,显示 loading 并加载数据
console.log(`%c🟡 [缓存] 目标页未缓存,需要加载第${newPage}页数据`, 'color: #DC2626; font-weight: bold;');
logger.debug('DynamicNewsCard', '目标页未缓存,加载数据', { newPage });
const success = await loadPage(newPage);
// 加载成功后切换页面
if (success) {
console.log(`%c🟢 [加载成功] 切换到第${newPage}`, 'color: #16A34A; font-weight: bold;');
setCurrentPage(newPage);
}
return;
}
// 【其他页】检查缓存
if (mode === DISPLAY_MODES.VERTICAL) {
// 纵向模式:检查页码映射中是否有缓存
const isPageCached = allCachedEventsByPage?.[newPage]?.length > 0;
console.log(`%c🟡 [缓存检查] 第${newPage}页缓存状态`, 'color: #EAB308; font-weight: bold;');
console.log(`%c 是否已缓存: ${isPageCached ? '✅ 是' : '❌ 否'}`, `color: ${isPageCached ? '#16A34A' : '#DC2626'};`);
if (isPageCached) {
console.log(`%c✅ [缓存] 第${newPage}页已缓存,直接切换`, 'color: #16A34A; font-weight: bold;');
setCurrentPage(newPage);
} else {
console.log(`%c❌ [加载失败] 未能切换到${newPage}`, 'color: #DC2626; font-weight: bold;');
console.log(`%c❌ [缓存] ${newPage}未缓存,加载数据`, 'color: #DC2626; font-weight: bold;');
const success = await loadPage(newPage, false); // clearCache = false
if (success) {
setCurrentPage(newPage);
}
}
}
}, [
currentPage,
pageSize,
totalPages,
total,
allCachedEvents.length,
cachedCount,
checkTargetPageCache,
loadPage,
dispatch,
toast
]);
// 更新累积列表(四排模式专用)
useEffect(() => {
if (isAccumulateMode) {
// 计算已加载的所有事件从第1页到当前页
const startIndex = 0;
const endIndex = currentPage * pageSize;
const accumulated = allCachedEvents.slice(startIndex, endIndex).filter(e => e !== null);
logger.debug('DynamicNewsCard', '更新累积事件列表', {
currentPage,
pageSize,
startIndex,
endIndex,
accumulatedLength: accumulated.length
});
setAccumulatedEvents(accumulated);
} else {
// 非累积模式时清空累积列表
if (accumulatedEvents.length > 0) {
setAccumulatedEvents([]);
// 平铺模式直接加载新页追加模式clearCache=false
console.log(`%c🟡 [平铺模式] 加载第${newPage}`, 'color: #EAB308; font-weight: bold;');
const success = await loadPage(newPage, false); // clearCache = false
if (success) {
setCurrentPage(newPage);
}
}
}, [isAccumulateMode, currentPage, pageSize, allCachedEvents]);
}, [mode, currentPage, totalPages, loadingPage, allCachedEventsByPage, loadPage]);
// 加载下一页(用于无限滚动)
const loadNextPage = useCallback(async () => {
// 修复:使用 hasMore 判断而不是 currentPage >= totalPages
// 原因:去重后 cachedCount 可能小于 total但 currentPage 已达到 totalPages
// 使用 hasMore 判断(基于 currentPage < totalPages
if (!hasMore || loadingPage !== null) {
logger.debug('DynamicNewsCard', '无法加载下一页', {
currentPage,
totalPages,
hasMore,
cachedCount,
total,
loadingPage,
reason: !hasMore ? '已加载全部数据 (cachedCount >= total)' : '正在加载中'
reason: !hasMore ? '已加载全部数据 (currentPage >= totalPages)' : '正在加载中'
});
return Promise.resolve(false); // 没有更多数据或正在加载
}
const nextPage = currentPage + 1;
logger.debug('DynamicNewsCard', '懒加载:加载下一页', { currentPage, nextPage, hasMore, cachedCount, total });
logger.debug('DynamicNewsCard', '懒加载:加载下一页', { currentPage, nextPage, hasMore, totalPages });
try {
await handlePageChange(nextPage);
@@ -285,7 +242,7 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
logger.error('DynamicNewsCard', '懒加载失败', error, { nextPage });
return false;
}
}, [currentPage, totalPages, hasMore, cachedCount, total, loadingPage, handlePageChange]);
}, [currentPage, totalPages, hasMore, loadingPage, handlePageChange]);
// 加载上一页(用于双向无限滚动)
const loadPrevPage = useCallback(async () => {
@@ -328,13 +285,12 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
totalPages,
hasMore,
currentPageEvents,
displayEvents, // 新增:当前显示的事件列表(累积或分页)
isAccumulateMode, // 新增:是否累积模式
displayEvents, // 当前显示的事件列表
// 方法
handlePageChange,
handleModeToggle,
loadNextPage, // 新增:加载下一页(用于无限滚动)
loadPrevPage // 新增:加载上一页(用于双向无限滚动)
loadNextPage, // 加载下一页(用于无限滚动)
loadPrevPage // 加载上一页(用于双向无限滚动)
};
};