// src/views/Community/components/DynamicNewsCard/hooks/usePagination.js // 分页逻辑自定义 Hook import { useState, useMemo, useCallback, useRef } from 'react'; import { fetchDynamicNews, updatePaginationPage } 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 {Object} options.allCachedEventsByPage - 纵向模式页码映射 { 1: [...], 2: [...] } * @param {Array} options.allCachedEvents - 平铺模式数组 [...] * @param {Object} options.pagination - 分页元数据 { total, total_pages, current_page, per_page, 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 - 筛选条件 * @param {string} options.initialMode - 初始显示模式(可选) * @returns {Object} 分页状态和方法 */ export const usePagination = ({ allCachedEventsByPage, // 纵向模式:页码映射 allCachedEvents, // 平铺模式:数组 pagination, // 分页元数据对象 total, // 向后兼容 cachedCount, dispatch, toast, filters = {}, initialMode // 初始显示模式 }) => { // 本地状态 const [loadingPage, setLoadingPage] = useState(null); const [mode, setMode] = useState(initialMode || DEFAULT_MODE); // 【核心改动】从 Redux pagination 派生 currentPage,不再使用本地状态 const currentPage = pagination?.current_page || PAGINATION_CONFIG.INITIAL_PAGE; // 使用 ref 存储最新的 filters,避免 useCallback 闭包问题 // 当 filters 对象引用不变但内容改变时,闭包中的 filters 是旧值 const filtersRef = useRef(filters); filtersRef.current = filters; // 根据模式决定每页显示数量 const pageSize = (() => { switch (mode) { 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.VERTICAL_PAGE_SIZE; } })(); // 【优化】优先使用后端返回的 total_pages,避免前端重复计算 // 向后兼容:如果没有 pagination 对象,则使用 total 计算 const totalPages = pagination?.total_pages || Math.ceil((pagination?.total || total || 0) / pageSize) || 1; // 检查是否还有更多数据(使用页码判断,不受去重影响) const hasMore = currentPage < totalPages; // 从页码映射或数组获取当前页数据 const currentPageEvents = useMemo(() => { 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; } }, [mode, allCachedEvents, currentPageEvents]); /** * 加载单个页面数据 * @param {number} targetPage - 目标页码 * @param {boolean} clearCache - 是否清空缓存(第1页专用) * @returns {Promise} 是否加载成功 */ 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}, clearCache=${clearCache}`, 'color: #16A34A;'); console.log(`%c 筛选条件:`, 'color: #16A34A;', filtersRef.current); logger.debug('DynamicNewsCard', '开始加载页面数据', { targetPage, pageSize, mode, clearCache, filters: filtersRef.current }); // 🔍 调试:dispatch 前 console.log(`%c🔵 [dispatch] 准备调用 fetchDynamicNews`, 'color: #3B82F6; font-weight: bold;', { mode, page: targetPage, per_page: pageSize, pageSize, clearCache, filters: filtersRef.current }); const result = await dispatch(fetchDynamicNews({ mode: mode, // 传递 mode 参数 per_page: pageSize, pageSize: pageSize, clearCache: clearCache, // 传递 clearCache 参数 ...filtersRef.current, // 从 ref 读取最新筛选条件 page: targetPage, // 然后覆盖 page 参数(避免被 filters.page 覆盖) })).unwrap(); // 🔍 调试:dispatch 后 console.log(`%c🔵 [dispatch] fetchDynamicNews 返回结果`, 'color: #3B82F6; font-weight: bold;', result); console.log(`%c🟢 [API请求] 第${targetPage}页加载完成`, 'color: #16A34A; font-weight: bold;'); logger.debug('DynamicNewsCard', `第 ${targetPage} 页加载完成`); return true; } catch (error) { logger.error('DynamicNewsCard', 'loadPage', error, { targetPage }); toast({ title: '加载失败', description: `无法加载第 ${targetPage} 页数据,请稍后重试`, status: 'error', duration: TOAST_CONFIG.DURATION_ERROR, isClosable: true, position: 'top' }); return false; } finally { setLoadingPage(null); } }, [dispatch, pageSize, toast, mode]); // 移除 filters 依赖,使用 filtersRef 读取最新值 // 翻页处理(第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 === newPage) { console.log(`%c⚠️ [翻页] 第${newPage}页正在加载中,忽略重复请求`, 'color: #EAB308; font-weight: bold;'); logger.warn('usePagination', '竞态条件:相同页面正在加载', { loadingPage, newPage }); return; } // 如果正在加载其他页面,允许切换(会取消当前加载状态,开始新的加载) if (loadingPage !== null && loadingPage !== newPage) { console.log(`%c🔄 [翻页] 正在加载第${loadingPage}页,用户切换到第${newPage}页`, 'color: #8B5CF6; font-weight: bold;'); logger.info('usePagination', '用户切换页面,继续处理新请求', { loadingPage, newPage }); // 继续执行,loadPage 会覆盖 loadingPage 状态 } console.log(`%c🔵 [翻页逻辑] handlePageChange 开始`, 'color: #3B82F6; font-weight: bold;'); console.log(`%c 当前页: ${currentPage}, 目标页: ${newPage}, 模式: ${mode}`, 'color: #3B82F6;'); // 【核心逻辑】第1页特殊处理:强制清空缓存并重新加载 if (newPage === 1) { console.log(`%c🔄 [第1页] 清空缓存并重新加载`, 'color: #8B5CF6; font-weight: bold;'); logger.info('usePagination', '第1页:强制刷新', { mode }); // clearCache = true:API 会更新 Redux pagination.current_page await loadPage(newPage, true); 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;'); // 使用缓存数据,同步更新 Redux pagination.current_page dispatch(updatePaginationPage({ mode, page: newPage })); } else { console.log(`%c❌ [缓存] 第${newPage}页未缓存,加载数据`, 'color: #DC2626; font-weight: bold;'); // clearCache = false:API 会更新 Redux pagination.current_page await loadPage(newPage, false); } } else { // 平铺模式:直接加载新页(追加模式,clearCache=false) console.log(`%c🟡 [平铺模式] 加载第${newPage}页`, 'color: #EAB308; font-weight: bold;'); // clearCache = false:API 会更新 Redux pagination.current_page await loadPage(newPage, false); } }, [mode, currentPage, totalPages, loadingPage, allCachedEventsByPage, loadPage]); // 加载下一页(用于无限滚动) const loadNextPage = useCallback(async () => { // 使用 hasMore 判断(基于 currentPage < totalPages) if (!hasMore || loadingPage !== null) { logger.debug('DynamicNewsCard', '无法加载下一页', { currentPage, totalPages, hasMore, loadingPage, reason: !hasMore ? '已加载全部数据 (currentPage >= totalPages)' : '正在加载中' }); return Promise.resolve(false); // 没有更多数据或正在加载 } const nextPage = currentPage + 1; logger.debug('DynamicNewsCard', '懒加载:加载下一页', { currentPage, nextPage, hasMore, totalPages }); try { await handlePageChange(nextPage); return true; } catch (error) { logger.error('DynamicNewsCard', '懒加载失败', error, { nextPage }); return false; } }, [currentPage, totalPages, hasMore, loadingPage, handlePageChange]); // 加载上一页(用于双向无限滚动) const loadPrevPage = useCallback(async () => { if (currentPage <= 1 || loadingPage !== null) { logger.debug('DynamicNewsCard', '无法加载上一页', { currentPage, loadingPage, reason: currentPage <= 1 ? '已是第一页' : '正在加载中' }); return Promise.resolve(false); // 已经是第一页或正在加载 } const prevPage = currentPage - 1; logger.debug('DynamicNewsCard', '懒加载:加载上一页', { currentPage, prevPage }); try { await handlePageChange(prevPage); return true; } catch (error) { logger.error('DynamicNewsCard', '懒加载上一页失败', error, { prevPage }); return false; } }, [currentPage, loadingPage, handlePageChange]); // 模式切换处理(简化版 - 模式切换时始终请求数据,因为两种模式使用独立存储) const handleModeToggle = useCallback((newMode) => { if (newMode === mode) return; setMode(newMode); // currentPage 由 Redux pagination.current_page 派生,会在下次请求时自动更新 // pageSize 会根据 mode 自动重新计算(第46-56行) }, [mode]); return { // 状态 currentPage, mode, loadingPage, pageSize, totalPages, hasMore, currentPageEvents, displayEvents, // 当前显示的事件列表 // 方法 handlePageChange, handleModeToggle, loadNextPage, // 加载下一页(用于无限滚动) loadPrevPage // 加载上一页(用于双向无限滚动) }; };