Files
vf_react/src/views/Community/components/DynamicNewsCard/hooks/usePagination.js

366 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/views/Community/components/DynamicNewsCard/hooks/usePagination.js
// 分页逻辑自定义 Hook
import { useState, useMemo, useCallback } from 'react';
import { fetchDynamicNews } from '../../../../../store/slices/communityDataSlice';
import { logger } from '../../../../../utils/logger';
import {
PAGINATION_CONFIG,
DISPLAY_MODES,
DEFAULT_MODE,
TOAST_CONFIG
} from '../constants';
/**
* 分页逻辑自定义 Hook
* @param {Object} options - Hook 配置选项
* @param {Array} options.allCachedEvents - 完整缓存事件列表
* @param {number} options.total - 服务端总数量
* @param {number} options.cachedCount - 已缓存数量
* @param {Function} options.dispatch - Redux dispatch 函数
* @param {Function} options.toast - Toast 通知函数
* @returns {Object} 分页状态和方法
*/
export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, toast }) => {
// 本地状态
const [currentPage, setCurrentPage] = useState(PAGINATION_CONFIG.INITIAL_PAGE);
const [loadingPage, setLoadingPage] = useState(null);
const [mode, setMode] = useState(DEFAULT_MODE);
// 根据模式决定每页显示数量
const pageSize = mode === DISPLAY_MODES.CAROUSEL
? PAGINATION_CONFIG.CAROUSEL_PAGE_SIZE
: PAGINATION_CONFIG.GRID_PAGE_SIZE;
// 计算总页数(基于服务端总数据量)
const totalPages = Math.ceil(total / pageSize) || 1;
// 检查是否还有更多数据
const hasMore = cachedCount < total;
// 从缓存中切片获取当前页数据(过滤 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]);
/**
* 子函数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);
const expectedCount = 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]);
/**
* 子函数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 (!silentMode) {
// 显示 loading 状态
setLoadingPage(targetPage);
}
try {
logger.debug('DynamicNewsCard', '开始加载页面数据', {
missingPages,
targetPage,
silentMode,
pageSize
});
// 拆分为单页请求,避免 per_page 动态值导致后端返回空数据
for (const page of missingPages) {
logger.debug('DynamicNewsCard', `开始加载第 ${page}`);
await dispatch(fetchDynamicNews({
page: page,
per_page: pageSize, // 固定值5或10不使用动态计算
pageSize: pageSize,
clearCache: false
})).unwrap();
logger.debug('DynamicNewsCard', `${page} 页加载完成`);
}
logger.debug('DynamicNewsCard', '所有页面加载完成', {
missingPages,
silentMode
});
return true;
} catch (error) {
logger.error('DynamicNewsCard', 'loadPages', error, {
targetPage,
silentMode,
missingPages
});
if (!silentMode) {
// 非静默模式下显示错误提示
toast({
title: '加载失败',
description: `无法加载第 ${targetPage} 页数据,请稍后重试`,
status: 'error',
duration: TOAST_CONFIG.DURATION_ERROR,
isClosable: true,
position: 'top'
});
}
return false;
} finally {
if (!silentMode) {
// 清除加载状态
setLoadingPage(null);
}
}
}, [dispatch, pageSize, toast]);
// 翻页处理(智能预加载)- 使用子函数重构
const handlePageChange = useCallback(async (newPage) => {
// 🔍 诊断日志 - 记录翻页开始状态
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
});
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); // 非静默模式
if (success) {
setCurrentPage(newPage);
}
} else if (missingPages.length === 0) {
// 场景C: 所有页面均已缓存,直接切换
logger.debug('DynamicNewsCard', '无需加载,直接切换', {
currentPage,
newPage,
reason: '所有页面均已缓存'
});
setCurrentPage(newPage);
} else {
// 场景D: 意外分支(有缺失页面但 hasMore=false
logger.warn('DynamicNewsCard', '意外分支:有缺失页面但无法加载', {
missingPages,
hasMore,
currentPage,
newPage,
total,
cachedCount
});
setCurrentPage(newPage);
toast({
title: '数据不完整',
description: `${newPage} 页数据可能不完整`,
status: 'warning',
duration: TOAST_CONFIG.DURATION_WARNING,
isClosable: true,
position: 'top'
});
}
}, [
currentPage,
pageSize,
totalPages,
hasMore,
total,
allCachedEvents.length,
cachedCount,
checkTargetPageCache,
calculatePreloadRange,
findMissingPages,
loadPages,
toast
]);
// 模式切换处理
const handleModeToggle = useCallback((newMode) => {
if (newMode === mode) return;
setMode(newMode);
setCurrentPage(PAGINATION_CONFIG.INITIAL_PAGE);
const newPageSize = newMode === DISPLAY_MODES.CAROUSEL
? PAGINATION_CONFIG.CAROUSEL_PAGE_SIZE
: PAGINATION_CONFIG.GRID_PAGE_SIZE;
// 检查第1页的数据是否完整排除 null
const firstPageData = allCachedEvents.slice(0, newPageSize);
const validFirstPageCount = firstPageData.filter(e => e !== null).length;
const needsRefetch = validFirstPageCount < Math.min(newPageSize, total);
if (needsRefetch) {
// 第1页数据不完整清空缓存重新请求
dispatch(fetchDynamicNews({
page: 1,
per_page: newPageSize,
pageSize: newPageSize, // 传递 pageSize 确保索引计算一致
clearCache: true
}));
}
// 如果第1页数据完整不发起请求直接切换
}, [mode, allCachedEvents, total, dispatch]);
return {
// 状态
currentPage,
mode,
loadingPage,
pageSize,
totalPages,
hasMore,
currentPageEvents,
// 方法
handlePageChange,
handleModeToggle
};
};