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

320 lines
13 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/DynamicNews/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;
// 根据模式决定每页显示数量
// mainline 模式复用 four-row 的分页配置
const pageSize = (() => {
switch (mode) {
case DISPLAY_MODES.FOUR_ROW:
case DISPLAY_MODES.MAINLINE:
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 || mode === DISPLAY_MODES.MAINLINE) {
// 平铺模式 / 主线模式:返回全部累积数据
return allCachedEvents || [];
} else {
// 纵向模式:返回当前页数据
return currentPageEvents;
}
}, [mode, allCachedEvents, currentPageEvents]);
/**
* 加载单个页面数据
* @param {number} targetPage - 目标页码
* @param {boolean} clearCache - 是否清空缓存第1页专用
* @returns {Promise<boolean>} 是否加载成功
*/
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
});
// mainline 模式使用 four-row 的 API 模式(共用同一份数据)
const apiMode = mode === DISPLAY_MODES.MAINLINE ? DISPLAY_MODES.FOUR_ROW : mode;
const result = await dispatch(fetchDynamicNews({
mode: apiMode, // 传递 API mode 参数mainline 映射为 four-row
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, force = false) => {
// force 参数:是否强制刷新(绕过"重复点击"检查)
// - true: 强制刷新Socket 新事件触发)
// - false: 正常翻页(用户点击分页按钮)
// 边界检查 1: 检查页码范围
if (newPage < 1 || newPage > totalPages) {
console.log(`%c⚠ [翻页] 页码超出范围: ${newPage}`, 'color: #DC2626; font-weight: bold;');
logger.warn('usePagination', '页码超出范围', { newPage, totalPages });
return;
}
// 边界检查 2: 检查是否重复点击(强制刷新时绕过此检查)
if (!force && newPage === currentPage) {
console.log(`%c⚠ [翻页] 重复点击当前页: ${newPage}`, 'color: #EAB308; font-weight: bold;');
logger.debug('usePagination', '页码未改变', { newPage });
return;
}
// ⚡ 如果是强制刷新force = true即使页码相同也继续执行
if (force && newPage === currentPage) {
console.log(`%c🔄 [翻页] 强制刷新当前页: ${newPage}`, 'color: #10B981; font-weight: bold;');
logger.info('usePagination', '强制刷新当前页', { newPage });
}
// 边界检查 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 = trueAPI 会更新 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 = falseAPI 会更新 Redux pagination.current_page
await loadPage(newPage, false);
}
} else {
// 平铺模式直接加载新页追加模式clearCache=false
console.log(`%c🟡 [平铺模式] 加载第${newPage}`, 'color: #EAB308; font-weight: bold;');
// clearCache = falseAPI 会更新 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 // 加载上一页(用于双向无限滚动)
};
};