feat: git commit -m "feat: 简化分页逻辑并添加累积模式支持 │ │
│ │ │ │ │ │ - 移除复杂的预加载逻辑(calculatePreloadRange、findMissingPages) │ │ │ │ - 添加累积显示模式(accumulatedEvents、isAccumulateMode) │ │ │ │ - 添加 displayEvents(累积或分页二选一) │ │ │ │ - 添加 loadNextPage 方法用于无限滚动 │ │ │ │ - 支持4种显示模式的pageSize计算 │ │ │ │ - 简化 handlePageChange 逻辑"
This commit is contained in:
@@ -22,25 +22,29 @@ import {
|
||||
* @returns {Object} 分页状态和方法
|
||||
*/
|
||||
export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, toast }) => {
|
||||
// 组件挂载状态跟踪 - 用于防止内存泄漏
|
||||
const isMountedRef = useRef(true);
|
||||
|
||||
// 本地状态
|
||||
const [currentPage, setCurrentPage] = useState(PAGINATION_CONFIG.INITIAL_PAGE);
|
||||
const [loadingPage, setLoadingPage] = useState(null);
|
||||
const [mode, setMode] = useState(DEFAULT_MODE);
|
||||
|
||||
// 组件卸载时更新挂载状态
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
// 累积显示的事件列表(用于四排模式的无限滚动)
|
||||
const [accumulatedEvents, setAccumulatedEvents] = useState([]);
|
||||
|
||||
// 根据模式决定每页显示数量
|
||||
const pageSize = mode === DISPLAY_MODES.CAROUSEL
|
||||
? PAGINATION_CONFIG.CAROUSEL_PAGE_SIZE
|
||||
: PAGINATION_CONFIG.GRID_PAGE_SIZE;
|
||||
const pageSize = (() => {
|
||||
switch (mode) {
|
||||
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;
|
||||
@@ -48,6 +52,9 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
|
||||
// 检查是否还有更多数据
|
||||
const hasMore = cachedCount < total;
|
||||
|
||||
// 判断是否使用累积模式(四排模式)
|
||||
const isAccumulateMode = mode === DISPLAY_MODES.FOUR_ROW;
|
||||
|
||||
// 从缓存中切片获取当前页数据(过滤 null 占位符)
|
||||
const currentPageEvents = useMemo(() => {
|
||||
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);
|
||||
}, [allCachedEvents, currentPage, pageSize]);
|
||||
|
||||
// 当前显示的事件列表(累积模式 vs 分页模式)
|
||||
const displayEvents = useMemo(() => {
|
||||
if (isAccumulateMode) {
|
||||
// 四排模式:累积显示所有已加载的事件
|
||||
return accumulatedEvents;
|
||||
} else {
|
||||
// 其他模式:只显示当前页
|
||||
return currentPageEvents;
|
||||
}
|
||||
}, [isAccumulateMode, accumulatedEvents, currentPageEvents]);
|
||||
|
||||
/**
|
||||
* 子函数1: 检查目标页缓存状态
|
||||
* @param {number} targetPage - 目标页码
|
||||
@@ -65,7 +83,10 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
|
||||
const targetPageEndIndex = targetPageStartIndex + pageSize;
|
||||
const targetPageData = allCachedEvents.slice(targetPageStartIndex, targetPageEndIndex);
|
||||
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;
|
||||
|
||||
logger.debug('DynamicNewsCard', '目标页缓存检查', {
|
||||
@@ -89,148 +110,44 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
|
||||
};
|
||||
}, [allCachedEvents, pageSize, total]);
|
||||
|
||||
// 已删除: calculatePreloadRange(不再需要预加载)
|
||||
|
||||
// 已删除: findMissingPages(不再需要查找缺失页面)
|
||||
|
||||
/**
|
||||
* 子函数2: 计算预加载范围
|
||||
* 加载单个页面数据
|
||||
* @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>} 是否加载成功
|
||||
*/
|
||||
const loadPages = useCallback(async (missingPages, targetPage, silentMode = false) => {
|
||||
// 检查组件是否已卸载
|
||||
if (!isMountedRef.current) {
|
||||
logger.debug('DynamicNewsCard', '组件已卸载,取消加载');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!silentMode) {
|
||||
const loadPage = useCallback(async (targetPage) => {
|
||||
// 显示 loading 状态
|
||||
setLoadingPage(targetPage);
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`%c🟢 [API请求] 开始加载第${targetPage}页数据`, 'color: #16A34A; font-weight: bold;');
|
||||
console.log(`%c 请求参数: page=${targetPage}, per_page=${pageSize}`, 'color: #16A34A;');
|
||||
|
||||
logger.debug('DynamicNewsCard', '开始加载页面数据', {
|
||||
missingPages,
|
||||
targetPage,
|
||||
silentMode,
|
||||
pageSize
|
||||
});
|
||||
|
||||
// 拆分为单页请求,避免 per_page 动态值导致后端返回空数据
|
||||
for (const page of missingPages) {
|
||||
// 每次请求前检查组件是否已卸载
|
||||
if (!isMountedRef.current) {
|
||||
logger.debug('DynamicNewsCard', '组件已卸载,中止加载');
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.debug('DynamicNewsCard', `开始加载第 ${page} 页`);
|
||||
|
||||
await dispatch(fetchDynamicNews({
|
||||
page: page,
|
||||
per_page: pageSize, // 固定值(5或10),不使用动态计算
|
||||
page: targetPage,
|
||||
per_page: pageSize,
|
||||
pageSize: pageSize,
|
||||
clearCache: false
|
||||
})).unwrap();
|
||||
|
||||
logger.debug('DynamicNewsCard', `第 ${page} 页加载完成`);
|
||||
}
|
||||
|
||||
logger.debug('DynamicNewsCard', '所有页面加载完成', {
|
||||
missingPages,
|
||||
silentMode
|
||||
});
|
||||
console.log(`%c🟢 [API请求] 第${targetPage}页加载完成`, 'color: #16A34A; font-weight: bold;');
|
||||
logger.debug('DynamicNewsCard', `第 ${targetPage} 页加载完成`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('DynamicNewsCard', 'loadPages', error, {
|
||||
targetPage,
|
||||
silentMode,
|
||||
missingPages
|
||||
logger.error('DynamicNewsCard', 'loadPage', error, {
|
||||
targetPage
|
||||
});
|
||||
|
||||
// 只在组件仍挂载时显示错误提示
|
||||
if (!silentMode && isMountedRef.current) {
|
||||
toast({
|
||||
title: '加载失败',
|
||||
description: `无法加载第 ${targetPage} 页数据,请稍后重试`,
|
||||
@@ -239,125 +156,131 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
// 只在组件仍挂载时清除加载状态
|
||||
if (!silentMode && isMountedRef.current) {
|
||||
setLoadingPage(null);
|
||||
}
|
||||
}
|
||||
}, [dispatch, pageSize, toast]);
|
||||
|
||||
// 翻页处理(智能预加载)- 使用子函数重构
|
||||
// 翻页处理(简化版 - 无预加载)
|
||||
const handlePageChange = useCallback(async (newPage) => {
|
||||
// 检查组件是否已卸载
|
||||
if (!isMountedRef.current) {
|
||||
logger.debug('DynamicNewsCard', '组件已卸载,取消翻页');
|
||||
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;');
|
||||
|
||||
// 🔍 诊断日志 - 记录翻页开始状态
|
||||
logger.debug('DynamicNewsCard', '开始翻页', {
|
||||
currentPage,
|
||||
newPage,
|
||||
pageSize,
|
||||
totalPages,
|
||||
hasMore,
|
||||
total,
|
||||
allCachedEventsLength: allCachedEvents.length,
|
||||
cachedCount
|
||||
});
|
||||
|
||||
// 步骤1: 检查目标页缓存状态
|
||||
const { isTargetPageCached } = checkTargetPageCache(newPage);
|
||||
|
||||
// 步骤2: 计算预加载范围
|
||||
const preloadRange = calculatePreloadRange(newPage, currentPage);
|
||||
|
||||
// 步骤3: 查找缺失页面
|
||||
const missingPages = findMissingPages(preloadRange);
|
||||
|
||||
// 步骤4: 根据情况加载数据
|
||||
if (isTargetPageCached && missingPages.length > 0 && hasMore) {
|
||||
// 场景A: 目标页已缓存,立即切换,后台静默预加载其他页
|
||||
logger.debug('DynamicNewsCard', '目标页已缓存,立即切换 + 后台预加载', {
|
||||
currentPage,
|
||||
newPage,
|
||||
缺失页面: missingPages
|
||||
});
|
||||
|
||||
// 只在组件仍挂载时更新状态
|
||||
if (isMountedRef.current) {
|
||||
setCurrentPage(newPage);
|
||||
// 特殊处理:返回第一页 - 清空缓存重新加载
|
||||
if (newPage === 1) {
|
||||
logger.debug('DynamicNewsCard', '返回第一页,清空缓存重新加载');
|
||||
setCurrentPage(1);
|
||||
dispatch(fetchDynamicNews({
|
||||
page: 1,
|
||||
per_page: pageSize,
|
||||
pageSize: pageSize,
|
||||
clearCache: true // 清空缓存
|
||||
}));
|
||||
return;
|
||||
}
|
||||
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); // 非静默模式
|
||||
// 只在加载成功且组件仍挂载时更新状态
|
||||
if (success && isMountedRef.current) {
|
||||
setCurrentPage(newPage);
|
||||
}
|
||||
} else if (missingPages.length === 0) {
|
||||
// 场景C: 所有页面均已缓存,直接切换
|
||||
logger.debug('DynamicNewsCard', '无需加载,直接切换', {
|
||||
currentPage,
|
||||
newPage,
|
||||
reason: '所有页面均已缓存'
|
||||
});
|
||||
// 检查目标页缓存状态
|
||||
const { isTargetPageCached, targetPageInfo } = checkTargetPageCache(newPage);
|
||||
|
||||
// 只在组件仍挂载时更新状态
|
||||
if (isMountedRef.current) {
|
||||
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 {
|
||||
// 场景D: 意外分支(有缺失页面但 hasMore=false)
|
||||
logger.warn('DynamicNewsCard', '意外分支:有缺失页面但无法加载', {
|
||||
missingPages,
|
||||
hasMore,
|
||||
currentPage,
|
||||
newPage,
|
||||
total,
|
||||
cachedCount
|
||||
});
|
||||
// 目标页未缓存,显示 loading 并加载数据
|
||||
console.log(`%c🟡 [缓存] 目标页未缓存,需要加载第${newPage}页数据`, 'color: #DC2626; font-weight: bold;');
|
||||
logger.debug('DynamicNewsCard', '目标页未缓存,加载数据', { newPage });
|
||||
const success = await loadPage(newPage);
|
||||
|
||||
// 只在组件仍挂载时更新状态
|
||||
if (isMountedRef.current) {
|
||||
// 加载成功后切换页面
|
||||
if (success) {
|
||||
console.log(`%c🟢 [加载成功] 切换到第${newPage}页`, 'color: #16A34A; font-weight: bold;');
|
||||
setCurrentPage(newPage);
|
||||
|
||||
toast({
|
||||
title: '数据不完整',
|
||||
description: `第 ${newPage} 页数据可能不完整`,
|
||||
status: 'warning',
|
||||
duration: TOAST_CONFIG.DURATION_WARNING,
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
} else {
|
||||
console.log(`%c❌ [加载失败] 未能切换到第${newPage}页`, 'color: #DC2626; font-weight: bold;');
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentPage,
|
||||
pageSize,
|
||||
totalPages,
|
||||
hasMore,
|
||||
total,
|
||||
allCachedEvents.length,
|
||||
cachedCount,
|
||||
checkTargetPageCache,
|
||||
calculatePreloadRange,
|
||||
findMissingPages,
|
||||
loadPages,
|
||||
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([]);
|
||||
}
|
||||
}
|
||||
}, [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) => {
|
||||
if (newMode === mode) return;
|
||||
@@ -365,9 +288,20 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
|
||||
setMode(newMode);
|
||||
setCurrentPage(PAGINATION_CONFIG.INITIAL_PAGE);
|
||||
|
||||
const newPageSize = newMode === DISPLAY_MODES.CAROUSEL
|
||||
? PAGINATION_CONFIG.CAROUSEL_PAGE_SIZE
|
||||
: PAGINATION_CONFIG.GRID_PAGE_SIZE;
|
||||
const newPageSize = (() => {
|
||||
switch (newMode) {
|
||||
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)
|
||||
const firstPageData = allCachedEvents.slice(0, newPageSize);
|
||||
@@ -395,9 +329,12 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
|
||||
totalPages,
|
||||
hasMore,
|
||||
currentPageEvents,
|
||||
displayEvents, // 新增:当前显示的事件列表(累积或分页)
|
||||
isAccumulateMode, // 新增:是否累积模式
|
||||
|
||||
// 方法
|
||||
handlePageChange,
|
||||
handleModeToggle
|
||||
handleModeToggle,
|
||||
loadNextPage // 新增:加载下一页(用于无限滚动)
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user