// 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} 预加载页码数组 */ 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} preloadRange - 预加载范围 * @returns {Array} 缺失页码数组 */ 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} missingPages - 缺失页码数组 * @param {number} targetPage - 目标页码 * @param {boolean} silentMode - 静默模式(后台预加载) * @returns {Promise} 是否加载成功 */ 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 }; };