feat: 提取常量和 Hooks 到独立文件(已完成)
This commit is contained in:
@@ -24,356 +24,8 @@ import EventScrollList from './DynamicNewsCard/EventScrollList';
|
|||||||
import DynamicNewsDetailPanel from './DynamicNewsDetail';
|
import DynamicNewsDetailPanel from './DynamicNewsDetail';
|
||||||
import UnifiedSearchBox from './UnifiedSearchBox';
|
import UnifiedSearchBox from './UnifiedSearchBox';
|
||||||
import { fetchDynamicNews, toggleEventFollow, selectEventFollowStatus } from '../../../store/slices/communityDataSlice';
|
import { fetchDynamicNews, toggleEventFollow, selectEventFollowStatus } from '../../../store/slices/communityDataSlice';
|
||||||
import { logger } from '../../../utils/logger';
|
import { usePagination } from './DynamicNewsCard/hooks/usePagination';
|
||||||
|
import { PAGINATION_CONFIG } from './DynamicNewsCard/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} 分页状态和方法
|
|
||||||
*/
|
|
||||||
const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, toast }) => {
|
|
||||||
// 本地状态
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const [loadingPage, setLoadingPage] = useState(null);
|
|
||||||
const [mode, setMode] = useState('carousel'); // 'carousel' 或 'grid'
|
|
||||||
|
|
||||||
// 根据模式决定每页显示数量
|
|
||||||
const pageSize = mode === 'carousel' ? 5 : 10;
|
|
||||||
|
|
||||||
// 计算总页数(基于服务端总数据量)
|
|
||||||
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) {
|
|
||||||
// 连续翻页:前后各2页(共5页)
|
|
||||||
const start = Math.max(1, targetPage - 2);
|
|
||||||
const end = Math.min(totalPages, targetPage + 2);
|
|
||||||
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: 3000,
|
|
||||||
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: 2000,
|
|
||||||
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(1);
|
|
||||||
|
|
||||||
const newPageSize = newMode === 'carousel' ? 5 : 10;
|
|
||||||
|
|
||||||
// 检查第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
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 实时要闻·动态追踪 - 事件展示卡片组件
|
* 实时要闻·动态追踪 - 事件展示卡片组件
|
||||||
@@ -443,9 +95,9 @@ const DynamicNewsCard = forwardRef(({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (allCachedEvents.length === 0) {
|
if (allCachedEvents.length === 0) {
|
||||||
dispatch(fetchDynamicNews({
|
dispatch(fetchDynamicNews({
|
||||||
page: 1,
|
page: PAGINATION_CONFIG.INITIAL_PAGE,
|
||||||
per_page: 5,
|
per_page: PAGINATION_CONFIG.CAROUSEL_PAGE_SIZE,
|
||||||
pageSize: 5, // 传递 pageSize 确保索引计算一致
|
pageSize: PAGINATION_CONFIG.CAROUSEL_PAGE_SIZE, // 传递 pageSize 确保索引计算一致
|
||||||
clearCache: true
|
clearCache: true
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/views/Community/components/DynamicNewsCard/constants.js
Normal file
24
src/views/Community/components/DynamicNewsCard/constants.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// src/views/Community/components/DynamicNewsCard/constants.js
|
||||||
|
// 动态新闻卡片组件 - 常量配置
|
||||||
|
|
||||||
|
// ========== 分页配置常量 ==========
|
||||||
|
export const PAGINATION_CONFIG = {
|
||||||
|
CAROUSEL_PAGE_SIZE: 5, // 单排模式每页数量
|
||||||
|
GRID_PAGE_SIZE: 10, // 双排模式每页数量
|
||||||
|
INITIAL_PAGE: 1, // 初始页码
|
||||||
|
PRELOAD_RANGE: 2, // 预加载范围(前后各N页)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== 显示模式常量 ==========
|
||||||
|
export const DISPLAY_MODES = {
|
||||||
|
CAROUSEL: 'carousel', // 单排轮播模式
|
||||||
|
GRID: 'grid', // 双排网格模式
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_MODE = DISPLAY_MODES.CAROUSEL;
|
||||||
|
|
||||||
|
// ========== Toast 提示配置 ==========
|
||||||
|
export const TOAST_CONFIG = {
|
||||||
|
DURATION_ERROR: 3000, // 错误提示持续时间(毫秒)
|
||||||
|
DURATION_WARNING: 2000, // 警告提示持续时间(毫秒)
|
||||||
|
};
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
// 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
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user