diff --git a/src/views/Community/components/DynamicNewsCard.js b/src/views/Community/components/DynamicNewsCard.js index 94c30b82..675b14ce 100644 --- a/src/views/Community/components/DynamicNewsCard.js +++ b/src/views/Community/components/DynamicNewsCard.js @@ -24,356 +24,8 @@ import EventScrollList from './DynamicNewsCard/EventScrollList'; import DynamicNewsDetailPanel from './DynamicNewsDetail'; import UnifiedSearchBox from './UnifiedSearchBox'; import { fetchDynamicNews, toggleEventFollow, selectEventFollowStatus } from '../../../store/slices/communityDataSlice'; -import { logger } from '../../../utils/logger'; - -/** - * 分页逻辑自定义 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} 预加载页码数组 - */ - 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} 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: 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 - }; -}; +import { usePagination } from './DynamicNewsCard/hooks/usePagination'; +import { PAGINATION_CONFIG } from './DynamicNewsCard/constants'; /** * 实时要闻·动态追踪 - 事件展示卡片组件 @@ -443,9 +95,9 @@ const DynamicNewsCard = forwardRef(({ useEffect(() => { if (allCachedEvents.length === 0) { dispatch(fetchDynamicNews({ - page: 1, - per_page: 5, - pageSize: 5, // 传递 pageSize 确保索引计算一致 + page: PAGINATION_CONFIG.INITIAL_PAGE, + per_page: PAGINATION_CONFIG.CAROUSEL_PAGE_SIZE, + pageSize: PAGINATION_CONFIG.CAROUSEL_PAGE_SIZE, // 传递 pageSize 确保索引计算一致 clearCache: true })); } diff --git a/src/views/Community/components/DynamicNewsCard/constants.js b/src/views/Community/components/DynamicNewsCard/constants.js new file mode 100644 index 00000000..c1df8422 --- /dev/null +++ b/src/views/Community/components/DynamicNewsCard/constants.js @@ -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, // 警告提示持续时间(毫秒) +}; diff --git a/src/views/Community/components/DynamicNewsCard/hooks/usePagination.js b/src/views/Community/components/DynamicNewsCard/hooks/usePagination.js new file mode 100644 index 00000000..eae13809 --- /dev/null +++ b/src/views/Community/components/DynamicNewsCard/hooks/usePagination.js @@ -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} 预加载页码数组 + */ + 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 + }; +};