feat: git commit -m "feat: 简化分页逻辑并添加累积模式支持 │ │

│ │                                                                                                                                                   │ │
│ │ - 移除复杂的预加载逻辑(calculatePreloadRange、findMissingPages)                                                                                 │ │
│ │ - 添加累积显示模式(accumulatedEvents、isAccumulateMode)                                                                                         │ │
│ │ - 添加 displayEvents(累积或分页二选一)                                                                                                          │ │
│ │ - 添加 loadNextPage 方法用于无限滚动                                                                                                              │ │
│ │ - 支持4种显示模式的pageSize计算                                                                                                                   │ │
│ │ - 简化 handlePageChange 逻辑"
This commit is contained in:
zdl
2025-11-05 08:42:10 +08:00
parent d96ebd6b8c
commit de1b31c70e

View File

@@ -22,25 +22,29 @@ import {
* @returns {Object} 分页状态和方法 * @returns {Object} 分页状态和方法
*/ */
export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, toast }) => { export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, toast }) => {
// 组件挂载状态跟踪 - 用于防止内存泄漏
const isMountedRef = useRef(true);
// 本地状态 // 本地状态
const [currentPage, setCurrentPage] = useState(PAGINATION_CONFIG.INITIAL_PAGE); const [currentPage, setCurrentPage] = useState(PAGINATION_CONFIG.INITIAL_PAGE);
const [loadingPage, setLoadingPage] = useState(null); const [loadingPage, setLoadingPage] = useState(null);
const [mode, setMode] = useState(DEFAULT_MODE); const [mode, setMode] = useState(DEFAULT_MODE);
// 组件卸载时更新挂载状态 // 累积显示的事件列表(用于四排模式的无限滚动)
useEffect(() => { const [accumulatedEvents, setAccumulatedEvents] = useState([]);
return () => {
isMountedRef.current = false;
};
}, []);
// 根据模式决定每页显示数量 // 根据模式决定每页显示数量
const pageSize = mode === DISPLAY_MODES.CAROUSEL const pageSize = (() => {
? PAGINATION_CONFIG.CAROUSEL_PAGE_SIZE switch (mode) {
: PAGINATION_CONFIG.GRID_PAGE_SIZE; case DISPLAY_MODES.CAROUSEL:
return PAGINATION_CONFIG.CAROUSEL_PAGE_SIZE;
case DISPLAY_MODES.GRID:
return PAGINATION_CONFIG.GRID_PAGE_SIZE;
case DISPLAY_MODES.FOUR_ROW:
return PAGINATION_CONFIG.FOUR_ROW_PAGE_SIZE;
case DISPLAY_MODES.VERTICAL:
return PAGINATION_CONFIG.VERTICAL_PAGE_SIZE;
default:
return PAGINATION_CONFIG.CAROUSEL_PAGE_SIZE;
}
})();
// 计算总页数(基于服务端总数据量) // 计算总页数(基于服务端总数据量)
const totalPages = Math.ceil(total / pageSize) || 1; const totalPages = Math.ceil(total / pageSize) || 1;
@@ -48,6 +52,9 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
// 检查是否还有更多数据 // 检查是否还有更多数据
const hasMore = cachedCount < total; const hasMore = cachedCount < total;
// 判断是否使用累积模式(四排模式)
const isAccumulateMode = mode === DISPLAY_MODES.FOUR_ROW;
// 从缓存中切片获取当前页数据(过滤 null 占位符) // 从缓存中切片获取当前页数据(过滤 null 占位符)
const currentPageEvents = useMemo(() => { const currentPageEvents = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize; const startIndex = (currentPage - 1) * pageSize;
@@ -55,6 +62,17 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
return allCachedEvents.slice(startIndex, endIndex).filter(event => event !== null); return allCachedEvents.slice(startIndex, endIndex).filter(event => event !== null);
}, [allCachedEvents, currentPage, pageSize]); }, [allCachedEvents, currentPage, pageSize]);
// 当前显示的事件列表(累积模式 vs 分页模式)
const displayEvents = useMemo(() => {
if (isAccumulateMode) {
// 四排模式:累积显示所有已加载的事件
return accumulatedEvents;
} else {
// 其他模式:只显示当前页
return currentPageEvents;
}
}, [isAccumulateMode, accumulatedEvents, currentPageEvents]);
/** /**
* 子函数1: 检查目标页缓存状态 * 子函数1: 检查目标页缓存状态
* @param {number} targetPage - 目标页码 * @param {number} targetPage - 目标页码
@@ -65,7 +83,10 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
const targetPageEndIndex = targetPageStartIndex + pageSize; const targetPageEndIndex = targetPageStartIndex + pageSize;
const targetPageData = allCachedEvents.slice(targetPageStartIndex, targetPageEndIndex); const targetPageData = allCachedEvents.slice(targetPageStartIndex, targetPageEndIndex);
const validTargetData = targetPageData.filter(e => e !== null); const validTargetData = targetPageData.filter(e => e !== null);
const expectedCount = Math.min(pageSize, total - targetPageStartIndex); // 修复:确保 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; const isTargetPageCached = validTargetData.length >= expectedCount;
logger.debug('DynamicNewsCard', '目标页缓存检查', { logger.debug('DynamicNewsCard', '目标页缓存检查', {
@@ -89,148 +110,44 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
}; };
}, [allCachedEvents, pageSize, total]); }, [allCachedEvents, pageSize, total]);
// 已删除: calculatePreloadRange不再需要预加载
// 已删除: findMissingPages不再需要查找缺失页面
/** /**
* 子函数2: 计算预加载范围 * 加载单个页面数据
* @param {number} targetPage - 目标页码 * @param {number} targetPage - 目标页码
* @param {number} fromPage - 来源页码
* @returns {Array<number>} 预加载页码数组
*/
const calculatePreloadRange = useCallback((targetPage, fromPage) => {
const isSequentialNavigation = Math.abs(targetPage - fromPage) === 1;
let preloadRange;
if (isSequentialNavigation) {
// 连续翻页前后各N页N = PRELOAD_RANGE
const start = Math.max(1, targetPage - PAGINATION_CONFIG.PRELOAD_RANGE);
const end = Math.min(totalPages, targetPage + PAGINATION_CONFIG.PRELOAD_RANGE);
preloadRange = Array.from(
{ length: end - start + 1 },
(_, i) => start + i
);
} else {
// 跳转翻页:只加载当前页
preloadRange = [targetPage];
}
logger.debug('DynamicNewsCard', '计算预加载范围', {
targetPage,
fromPage,
isSequentialNavigation,
preloadRange
});
return preloadRange;
}, [totalPages]);
/**
* 子函数3: 查找缺失页面
* @param {Array<number>} preloadRange - 预加载范围
* @returns {Array<number>} 缺失页码数组
*/
const findMissingPages = useCallback((preloadRange) => {
const missingPages = preloadRange.filter(page => {
const pageStartIndex = (page - 1) * pageSize;
const pageEndIndex = pageStartIndex + pageSize;
// 如果该页超出数组范围,说明未缓存
if (pageEndIndex > allCachedEvents.length) {
logger.debug('DynamicNewsCard', `页面${page}超出数组范围`, {
pageStartIndex,
pageEndIndex,
allCachedEventsLength: allCachedEvents.length
});
return true;
}
// 检查该页的数据是否包含 null 占位符或数据不足
const pageData = allCachedEvents.slice(pageStartIndex, pageEndIndex);
const validData = pageData.filter(e => e !== null);
const expectedCount = Math.min(pageSize, total - pageStartIndex);
const hasNullOrIncomplete = validData.length < expectedCount;
logger.debug('DynamicNewsCard', `页面${page}数据检查`, {
pageStartIndex,
pageEndIndex,
pageDataLength: pageData.length,
validDataLength: validData.length,
expectedCount,
hasNullOrIncomplete
});
return hasNullOrIncomplete;
});
logger.debug('DynamicNewsCard', '缺失页面检测完成', {
preloadRange,
missingPages,
missingPagesCount: missingPages.length
});
return missingPages;
}, [allCachedEvents, pageSize, total]);
/**
* 子函数4: 加载页面数据
* @param {Array<number>} missingPages - 缺失页码数组
* @param {number} targetPage - 目标页码
* @param {boolean} silentMode - 静默模式(后台预加载)
* @returns {Promise<boolean>} 是否加载成功 * @returns {Promise<boolean>} 是否加载成功
*/ */
const loadPages = useCallback(async (missingPages, targetPage, silentMode = false) => { const loadPage = useCallback(async (targetPage) => {
// 检查组件是否已卸载
if (!isMountedRef.current) {
logger.debug('DynamicNewsCard', '组件已卸载,取消加载');
return false;
}
if (!silentMode) {
// 显示 loading 状态 // 显示 loading 状态
setLoadingPage(targetPage); setLoadingPage(targetPage);
}
try { try {
console.log(`%c🟢 [API请求] 开始加载第${targetPage}页数据`, 'color: #16A34A; font-weight: bold;');
console.log(`%c 请求参数: page=${targetPage}, per_page=${pageSize}`, 'color: #16A34A;');
logger.debug('DynamicNewsCard', '开始加载页面数据', { logger.debug('DynamicNewsCard', '开始加载页面数据', {
missingPages,
targetPage, targetPage,
silentMode,
pageSize pageSize
}); });
// 拆分为单页请求,避免 per_page 动态值导致后端返回空数据
for (const page of missingPages) {
// 每次请求前检查组件是否已卸载
if (!isMountedRef.current) {
logger.debug('DynamicNewsCard', '组件已卸载,中止加载');
return false;
}
logger.debug('DynamicNewsCard', `开始加载第 ${page}`);
await dispatch(fetchDynamicNews({ await dispatch(fetchDynamicNews({
page: page, page: targetPage,
per_page: pageSize, // 固定值5或10不使用动态计算 per_page: pageSize,
pageSize: pageSize, pageSize: pageSize,
clearCache: false clearCache: false
})).unwrap(); })).unwrap();
logger.debug('DynamicNewsCard', `${page} 页加载完成`); console.log(`%c🟢 [API请求] 第${targetPage}页加载完成`, 'color: #16A34A; font-weight: bold;');
} logger.debug('DynamicNewsCard', `${targetPage} 页加载完成`);
logger.debug('DynamicNewsCard', '所有页面加载完成', {
missingPages,
silentMode
});
return true; return true;
} catch (error) { } catch (error) {
logger.error('DynamicNewsCard', 'loadPages', error, { logger.error('DynamicNewsCard', 'loadPage', error, {
targetPage, targetPage
silentMode,
missingPages
}); });
// 只在组件仍挂载时显示错误提示
if (!silentMode && isMountedRef.current) {
toast({ toast({
title: '加载失败', title: '加载失败',
description: `无法加载第 ${targetPage} 页数据,请稍后重试`, description: `无法加载第 ${targetPage} 页数据,请稍后重试`,
@@ -239,125 +156,131 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
isClosable: true, isClosable: true,
position: 'top' position: 'top'
}); });
}
return false; return false;
} finally { } finally {
// 只在组件仍挂载时清除加载状态
if (!silentMode && isMountedRef.current) {
setLoadingPage(null); setLoadingPage(null);
} }
}
}, [dispatch, pageSize, toast]); }, [dispatch, pageSize, toast]);
// 翻页处理(智能预加载)- 使用子函数重构 // 翻页处理(简化版 - 无预加载)
const handlePageChange = useCallback(async (newPage) => { const handlePageChange = useCallback(async (newPage) => {
// 检查组件是否已卸载 console.log(`%c🔵 [翻页逻辑] handlePageChange 开始`, 'color: #3B82F6; font-weight: bold;');
if (!isMountedRef.current) { console.log(`%c 当前页: ${currentPage}, 目标页: ${newPage}, 总页数: ${totalPages}`, 'color: #3B82F6;');
logger.debug('DynamicNewsCard', '组件已卸载,取消翻页'); console.log(`%c 每页大小: ${pageSize}, 缓存总数: ${allCachedEvents.length}, 服务端总数: ${total}`, 'color: #3B82F6;');
return;
}
// 🔍 诊断日志 - 记录翻页开始状态
logger.debug('DynamicNewsCard', '开始翻页', { logger.debug('DynamicNewsCard', '开始翻页', {
currentPage, currentPage,
newPage, newPage,
pageSize, pageSize,
totalPages, totalPages,
hasMore,
total, total,
allCachedEventsLength: allCachedEvents.length, allCachedEventsLength: allCachedEvents.length,
cachedCount cachedCount
}); });
// 步骤1: 检查目标页缓存状态 // 特殊处理:返回第一页 - 清空缓存重新加载
const { isTargetPageCached } = checkTargetPageCache(newPage); if (newPage === 1) {
logger.debug('DynamicNewsCard', '返回第一页,清空缓存重新加载');
// 步骤2: 计算预加载范围 setCurrentPage(1);
const preloadRange = calculatePreloadRange(newPage, currentPage); dispatch(fetchDynamicNews({
page: 1,
// 步骤3: 查找缺失页面 per_page: pageSize,
const missingPages = findMissingPages(preloadRange); pageSize: pageSize,
clearCache: true // 清空缓存
// 步骤4: 根据情况加载数据 }));
if (isTargetPageCached && missingPages.length > 0 && hasMore) { return;
// 场景A: 目标页已缓存,立即切换,后台静默预加载其他页
logger.debug('DynamicNewsCard', '目标页已缓存,立即切换 + 后台预加载', {
currentPage,
newPage,
缺失页面: missingPages
});
// 只在组件仍挂载时更新状态
if (isMountedRef.current) {
setCurrentPage(newPage);
} }
await loadPages(missingPages, newPage, true); // 静默模式
} else if (missingPages.length > 0 && hasMore) {
// 场景B: 目标页未缓存,显示 loading 并等待加载完成
logger.debug('DynamicNewsCard', '目标页未缓存,显示 loading', {
currentPage,
newPage,
缺失页面: missingPages
});
const success = await loadPages(missingPages, newPage, false); // 非静默模式 // 检查目标页缓存状态
// 只在加载成功且组件仍挂载时更新状态 const { isTargetPageCached, targetPageInfo } = checkTargetPageCache(newPage);
if (success && isMountedRef.current) {
setCurrentPage(newPage);
}
} else if (missingPages.length === 0) {
// 场景C: 所有页面均已缓存,直接切换
logger.debug('DynamicNewsCard', '无需加载,直接切换', {
currentPage,
newPage,
reason: '所有页面均已缓存'
});
// 只在组件仍挂载时更新状态 console.log(`%c🟡 [缓存检查] 目标页${newPage}缓存状态`, 'color: #EAB308; font-weight: bold;');
if (isMountedRef.current) { 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); setCurrentPage(newPage);
}
} else { } else {
// 场景D: 意外分支(有缺失页面但 hasMore=false // 目标页未缓存,显示 loading 并加载数据
logger.warn('DynamicNewsCard', '意外分支:有缺失页面但无法加载', { console.log(`%c🟡 [缓存] 目标页未缓存,需要加载第${newPage}页数据`, 'color: #DC2626; font-weight: bold;');
missingPages, logger.debug('DynamicNewsCard', '目标页未缓存,加载数据', { newPage });
hasMore, const success = await loadPage(newPage);
currentPage,
newPage,
total,
cachedCount
});
// 只在组件仍挂载时更新状态 // 加载成功后切换页面
if (isMountedRef.current) { if (success) {
console.log(`%c🟢 [加载成功] 切换到第${newPage}`, 'color: #16A34A; font-weight: bold;');
setCurrentPage(newPage); setCurrentPage(newPage);
} else {
toast({ console.log(`%c❌ [加载失败] 未能切换到第${newPage}`, 'color: #DC2626; font-weight: bold;');
title: '数据不完整',
description: `${newPage} 页数据可能不完整`,
status: 'warning',
duration: TOAST_CONFIG.DURATION_WARNING,
isClosable: true,
position: 'top'
});
} }
} }
}, [ }, [
currentPage, currentPage,
pageSize, pageSize,
totalPages, totalPages,
hasMore,
total, total,
allCachedEvents.length, allCachedEvents.length,
cachedCount, cachedCount,
checkTargetPageCache, checkTargetPageCache,
calculatePreloadRange, loadPage,
findMissingPages, dispatch,
loadPages,
toast 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([]);
}
}
}, [isAccumulateMode, currentPage, pageSize, allCachedEvents]);
// 加载下一页(用于无限滚动)
const loadNextPage = useCallback(async () => {
if (currentPage >= totalPages || loadingPage !== null) {
logger.debug('DynamicNewsCard', '无法加载下一页', {
currentPage,
totalPages,
loadingPage,
reason: currentPage >= totalPages ? '已是最后一页' : '正在加载中'
});
return Promise.resolve(false); // 没有更多数据或正在加载
}
const nextPage = currentPage + 1;
logger.debug('DynamicNewsCard', '懒加载:加载下一页', { currentPage, nextPage });
try {
await handlePageChange(nextPage);
return true;
} catch (error) {
logger.error('DynamicNewsCard', '懒加载失败', error, { nextPage });
return false;
}
}, [currentPage, totalPages, loadingPage, handlePageChange]);
// 模式切换处理 // 模式切换处理
const handleModeToggle = useCallback((newMode) => { const handleModeToggle = useCallback((newMode) => {
if (newMode === mode) return; if (newMode === mode) return;
@@ -365,9 +288,20 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
setMode(newMode); setMode(newMode);
setCurrentPage(PAGINATION_CONFIG.INITIAL_PAGE); setCurrentPage(PAGINATION_CONFIG.INITIAL_PAGE);
const newPageSize = newMode === DISPLAY_MODES.CAROUSEL const newPageSize = (() => {
? PAGINATION_CONFIG.CAROUSEL_PAGE_SIZE switch (newMode) {
: PAGINATION_CONFIG.GRID_PAGE_SIZE; case DISPLAY_MODES.CAROUSEL:
return PAGINATION_CONFIG.CAROUSEL_PAGE_SIZE;
case DISPLAY_MODES.GRID:
return PAGINATION_CONFIG.GRID_PAGE_SIZE;
case DISPLAY_MODES.FOUR_ROW:
return PAGINATION_CONFIG.FOUR_ROW_PAGE_SIZE;
case DISPLAY_MODES.VERTICAL:
return PAGINATION_CONFIG.VERTICAL_PAGE_SIZE;
default:
return PAGINATION_CONFIG.CAROUSEL_PAGE_SIZE;
}
})();
// 检查第1页的数据是否完整排除 null // 检查第1页的数据是否完整排除 null
const firstPageData = allCachedEvents.slice(0, newPageSize); const firstPageData = allCachedEvents.slice(0, newPageSize);
@@ -395,9 +329,12 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
totalPages, totalPages,
hasMore, hasMore,
currentPageEvents, currentPageEvents,
displayEvents, // 新增:当前显示的事件列表(累积或分页)
isAccumulateMode, // 新增:是否累积模式
// 方法 // 方法
handlePageChange, handlePageChange,
handleModeToggle handleModeToggle,
loadNextPage // 新增:加载下一页(用于无限滚动)
}; };
}; };