diff --git a/src/services/eventService.js b/src/services/eventService.js index 6f365d70..87c76ecd 100755 --- a/src/services/eventService.js +++ b/src/services/eventService.js @@ -37,10 +37,12 @@ const apiRequest = async (url, options = {}) => { export const eventService = { getEvents: (params = {}) => { - // Filter out empty params - const cleanParams = Object.fromEntries(Object.entries(params).filter(([_, v]) => v != null && v !== '')); + // Filter out null, undefined, and empty strings (but keep 0 and false) + const cleanParams = Object.fromEntries( + Object.entries(params).filter(([_, v]) => v !== null && v !== undefined && v !== '') + ); const query = new URLSearchParams(cleanParams).toString(); - return apiRequest(`/api/events/?${query}`); + return apiRequest(`/api/events?${query}`); }, getHotEvents: (params = {}) => { const query = new URLSearchParams(params).toString(); diff --git a/src/store/slices/communityDataSlice.js b/src/store/slices/communityDataSlice.js index 3c8205a5..de6bdf00 100644 --- a/src/store/slices/communityDataSlice.js +++ b/src/store/slices/communityDataSlice.js @@ -222,10 +222,18 @@ export const fetchDynamicNews = createAsyncThunk( total: response.data.pagination?.total || 0, per_page: finalPerPage }); + // 【兜底处理】支持多种 pagination 字段名:pages (后端) / total_pages (旧Mock) / totalPages + const paginationData = response.data.pagination || {}; + const calculatedTotalPages = paginationData.pages // ← 后端格式 (优先) + || paginationData.total_pages // ← Mock 旧格式 (兼容) + || paginationData.totalPages // ← 其他可能格式 + || Math.ceil((paginationData.total || 0) / finalPerPage); // ← 兜底计算 + return { mode, events: response.data.events, - total: response.data.pagination?.total || 0, + total: paginationData.total || 0, + totalPages: calculatedTotalPages, page, per_page: finalPerPage, clearCache, @@ -234,10 +242,15 @@ export const fetchDynamicNews = createAsyncThunk( } logger.warn('CommunityData', '动态新闻返回数据为空', response); + // 【兜底处理】空数据情况也尝试读取 pagination + const emptyPaginationData = response.data?.pagination || {}; + const emptyTotalPages = emptyPaginationData.pages || emptyPaginationData.total_pages || 0; + return { mode, events: [], total: 0, + totalPages: emptyTotalPages, page, per_page: finalPerPage, clearCache, @@ -327,12 +340,22 @@ const communityDataSlice = createSlice({ hotEvents: [], // 【纵向模式】独立存储(传统分页 + 每页10条) - verticalEvents: [], // 完整缓存列表(累积所有已加载数据) - verticalTotal: 0, // 服务端总数量(用于计算总页数) + verticalEventsByPage: {}, // 页码映射存储 { 1: [10条], 2: [8条], 3: [10条] } + verticalPagination: { // 分页元数据 + total: 0, // 总记录数 + total_pages: 0, // 总页数 + current_page: 1, // 当前页码 + per_page: 10 // 每页大小 + }, // 【平铺模式】独立存储(虚拟滚动 + 每页30条) fourRowEvents: [], // 完整缓存列表(虚拟滚动的数据源) - fourRowTotal: 0, // 服务端总数量(用于判断 hasMore) + fourRowPagination: { // 分页元数据 + total: 0, // 总记录数 + total_pages: 0, // 总页数 + current_page: 1, // 当前页码 + per_page: 30 // 每页大小 + }, eventFollowStatus: {}, // 事件关注状态(全局共享){ [eventId]: { isFollowing: boolean, followerCount: number } } @@ -367,10 +390,10 @@ const communityDataSlice = createSlice({ state.hotEvents = []; // 清除动态新闻数据(两个模式) - state.verticalEvents = []; + state.verticalEventsByPage = {}; state.fourRowEvents = []; - state.verticalTotal = 0; - state.fourRowTotal = 0; + state.verticalPagination = { total: 0, total_pages: 0, current_page: 1, per_page: 10 }; + state.fourRowPagination = { total: 0, total_pages: 0, current_page: 1, per_page: 30 }; logger.info('CommunityData', '所有缓存已清除'); }, @@ -392,13 +415,13 @@ const communityDataSlice = createSlice({ logger.info('CommunityData', '热点事件缓存已清除'); } else if (type === 'verticalEvents') { // verticalEvents 不使用 localStorage,只清除 Redux state - state.verticalEvents = []; - state.verticalTotal = 0; + state.verticalEventsByPage = {}; + state.verticalPagination = { total: 0, total_pages: 0, current_page: 1, per_page: 10 }; logger.info('CommunityData', '纵向模式事件数据已清除'); } else if (type === 'fourRowEvents') { // fourRowEvents 不使用 localStorage,只清除 Redux state state.fourRowEvents = []; - state.fourRowTotal = 0; + state.fourRowPagination = { total: 0, total_pages: 0, current_page: 1, per_page: 30 }; logger.info('CommunityData', '平铺模式事件数据已清除'); } }, @@ -438,7 +461,7 @@ const communityDataSlice = createSlice({ state.error[stateKey] = null; }) .addCase(fetchDynamicNews.fulfilled, (state, action) => { - const { mode, events, total, page, clearCache, prependMode, isEmpty } = action.payload; + const { mode, events, total, page, per_page, clearCache, prependMode, isEmpty } = action.payload; const stateKey = mode === 'four-row' ? 'fourRowEvents' : 'verticalEvents'; const totalKey = mode === 'four-row' ? 'fourRowTotal' : 'verticalTotal'; @@ -459,63 +482,103 @@ const communityDataSlice = createSlice({ page, clearCache, prependMode, - 'state[stateKey] 之前': state[stateKey].length, + 'state[stateKey] 类型': Array.isArray(state[stateKey]) ? 'Array' : 'Object', + 'state[stateKey] 之前': Array.isArray(state[stateKey]) + ? `数组长度: ${state[stateKey].length}` + : `对象页数: ${Object.keys(state[stateKey] || {}).length}`, }); /** - * 【数据去重和追加逻辑】 + * 【数据存储逻辑】根据模式选择不同的存储策略 * - * 三种模式: - * 1. clearCache 模式:直接替换(用于刷新或模式切换) - * 2. prependMode 模式:去重后插入头部(用于定时刷新,获取最新事件) - * 3. append 模式(默认):去重后追加到末尾(用于无限滚动加载下一页) + * 纵向模式(vertical):页码映射存储 + * - clearCache=true: 清空所有页,存储新页(第1页专用) + * - clearCache=false: 存储到对应页码(第2、3、4...页) + * - 优点:每页独立,不受去重影响,支持缓存 * - * 去重逻辑(append 和 prepend 模式): - * - 使用 Set 提取已存在的事件 ID - * - 过滤掉新数据中与现有数据重复的事件 - * - 只保留真正的新事件 - * - * 为什么需要去重: - * 1. 网络请求乱序:例如第3页比第2页先返回 - * 2. 定时刷新冲突:用户正在浏览时后台刷新了第一页 - * 3. 后端分页漂移:新事件插入导致页码边界变化 + * 平铺模式(four-row):去重追加存储 + * - clearCache=true: 直接替换(用于刷新) + * - prependMode=true: 去重后插入头部(定时刷新) + * - 默认:去重后追加到末尾(无限滚动) + * - 优点:累积显示,支持虚拟滚动 */ - if (clearCache) { - // 清空缓存模式:直接替换 - state[stateKey] = events; - logger.debug('CommunityData', `清空缓存并加载新数据 (${mode})`, { - count: events.length - }); + if (mode === 'vertical') { + // 【纵向模式】页码映射存储 + if (clearCache) { + // 第1页:清空所有页,只保留新页 + state.verticalEventsByPage = { [page]: events }; + logger.debug('CommunityData', `清空缓存并加载第${page}页 (纵向模式)`, { + count: events.length + }); + console.log('%c[Redux] 纵向模式 clearCache,清空所有页', 'color: #10B981; font-weight: bold;', { + page, + eventsCount: events.length + }); + } else { + // 其他页:存储到对应页码 + state.verticalEventsByPage = state.verticalEventsByPage || {}; + state.verticalEventsByPage[page] = events; + logger.debug('CommunityData', `存储第${page}页数据 (纵向模式)`, { + page, + count: events.length, + totalPages: Object.keys(state.verticalEventsByPage || {}).length + }); + console.log('%c[Redux] 纵向模式追加页面', 'color: #10B981; font-weight: bold;', { + page, + eventsCount: events.length, + cachedPages: Object.keys(state.verticalEventsByPage || {}) + }); + } + } else if (mode === 'four-row') { + // 【平铺模式】去重追加存储 + if (clearCache) { + // 清空缓存模式:直接替换 + state.fourRowEvents = events; + logger.debug('CommunityData', `清空缓存并加载新数据 (平铺模式)`, { + count: events.length + }); + console.log('%c[Redux] 平铺模式 clearCache,直接替换数据', 'color: #10B981; font-weight: bold;', { + eventsCount: events.length + }); + } else if (prependMode) { + // 追加到头部模式(用于定时刷新):去重后插入头部 + const existingIds = new Set((state.fourRowEvents || []).map(e => e.id)); + const newEvents = events.filter(e => !existingIds.has(e.id)); + state.fourRowEvents = [...newEvents, ...(state.fourRowEvents || [])]; + logger.debug('CommunityData', `追加新数据到头部 (平铺模式)`, { + newCount: newEvents.length, + totalCount: state.fourRowEvents.length + }); + } else { + // 默认追加模式:去重后追加到末尾(用于虚拟滚动加载下一页) + const existingIds = new Set((state.fourRowEvents || []).map(e => e.id)); + const newEvents = events.filter(e => !existingIds.has(e.id)); + state.fourRowEvents = [...(state.fourRowEvents || []), ...newEvents]; - // 🔍 调试:清空缓存后的状态 - console.log('%c[Redux] clearCache 模式,直接替换数据', 'color: #10B981; font-weight: bold;', { - 'state[stateKey] 之后': state[stateKey].length - }); - } else if (prependMode) { - // 追加到头部模式(用于定时刷新):去重后插入头部 - const existingIds = new Set(state[stateKey].map(e => e.id)); - const newEvents = events.filter(e => !existingIds.has(e.id)); - state[stateKey] = [...newEvents, ...state[stateKey]]; - logger.debug('CommunityData', `追加新数据到头部 (${mode})`, { - newCount: newEvents.length, - totalCount: state[stateKey].length - }); - } else { - // 默认追加模式:去重后追加到末尾(用于虚拟滚动加载下一页) - const existingIds = new Set(state[stateKey].map(e => e.id)); - const newEvents = events.filter(e => !existingIds.has(e.id)); - state[stateKey] = [...state[stateKey], ...newEvents]; - - logger.debug('CommunityData', `追加新数据(去重,${mode})`, { - page, - originalEventsCount: events.length, - newEventsCount: newEvents.length, - filteredCount: events.length - newEvents.length, - totalCount: state[stateKey].length - }); + logger.debug('CommunityData', `追加新数据(去重,平铺模式)`, { + page, + originalEventsCount: events.length, + newEventsCount: newEvents.length, + filteredCount: events.length - newEvents.length, + totalCount: state.fourRowEvents.length + }); + } } - state[totalKey] = total; + // 【元数据存储】存储完整的 pagination 对象 + const paginationKey = mode === 'four-row' ? 'fourRowPagination' : 'verticalPagination'; + const finalPerPage = per_page || (mode === 'four-row' ? 30 : 10); // 兜底默认值 + state[paginationKey] = { + total: total, + total_pages: action.payload.totalPages || Math.ceil(total / finalPerPage), + current_page: page, + per_page: finalPerPage + }; + + console.log('%c[Redux] 更新分页元数据', 'color: #8B5CF6; font-weight: bold;', { + mode, + pagination: state[paginationKey] + }); state.loading[stateKey] = false; }) @@ -550,14 +613,18 @@ export const selectLoading = (state) => state.communityData.loading; export const selectError = (state) => state.communityData.error; // 纵向模式数据选择器 -export const selectVerticalEvents = (state) => state.communityData.verticalEvents; -export const selectVerticalTotal = (state) => state.communityData.verticalTotal; -export const selectVerticalCachedCount = (state) => state.communityData.verticalEvents.length; +export const selectVerticalEventsByPage = (state) => state.communityData.verticalEventsByPage; +export const selectVerticalPagination = (state) => state.communityData.verticalPagination; +export const selectVerticalCachedPageCount = (state) => Object.keys(state.communityData.verticalEventsByPage || {}).length; // 平铺模式数据选择器 export const selectFourRowEvents = (state) => state.communityData.fourRowEvents; -export const selectFourRowTotal = (state) => state.communityData.fourRowTotal; -export const selectFourRowCachedCount = (state) => state.communityData.fourRowEvents.length; +export const selectFourRowPagination = (state) => state.communityData.fourRowPagination; +export const selectFourRowCachedCount = (state) => (state.communityData.fourRowEvents || []).length; + +// 向后兼容的选择器(已废弃,建议使用 selectVerticalPagination.total) +export const selectVerticalTotal = (state) => state.communityData.verticalPagination?.total || 0; +export const selectFourRowTotal = (state) => state.communityData.fourRowPagination?.total || 0; // 组合选择器 export const selectPopularKeywordsWithLoading = (state) => ({ @@ -574,11 +641,12 @@ export const selectHotEventsWithLoading = (state) => ({ // 纵向模式数据 + 加载状态选择器 export const selectVerticalEventsWithLoading = (state) => ({ - data: state.communityData.verticalEvents, // 完整缓存列表 + data: state.communityData.verticalEventsByPage, // 页码映射 { 1: [...], 2: [...] } loading: state.communityData.loading.verticalEvents, error: state.communityData.error.verticalEvents, - total: state.communityData.verticalTotal, // 服务端总数量 - cachedCount: state.communityData.verticalEvents.length // 已缓存有效数量 + pagination: state.communityData.verticalPagination, // 完整分页元数据 { total, total_pages, current_page, per_page } + total: state.communityData.verticalPagination?.total || 0, // 向后兼容:服务端总数量 + cachedPageCount: Object.keys(state.communityData.verticalEventsByPage || {}).length // 已缓存页数 }); // 平铺模式数据 + 加载状态选择器 @@ -586,8 +654,9 @@ export const selectFourRowEventsWithLoading = (state) => ({ data: state.communityData.fourRowEvents, // 完整缓存列表 loading: state.communityData.loading.fourRowEvents, error: state.communityData.error.fourRowEvents, - total: state.communityData.fourRowTotal, // 服务端总数量 - cachedCount: state.communityData.fourRowEvents.length // 已缓存有效数量 + pagination: state.communityData.fourRowPagination, // 完整分页元数据 { total, total_pages, current_page, per_page } + total: state.communityData.fourRowPagination?.total || 0, // 向后兼容:服务端总数量 + cachedCount: (state.communityData.fourRowEvents || []).length // 已缓存有效数量 }); export default communityDataSlice.reducer; diff --git a/src/views/Community/components/DynamicNewsCard.js b/src/views/Community/components/DynamicNewsCard.js index ac83554f..d047d40a 100644 --- a/src/views/Community/components/DynamicNewsCard.js +++ b/src/views/Community/components/DynamicNewsCard.js @@ -82,36 +82,48 @@ const DynamicNewsCard = forwardRef(({ // 🔍 调试:从 Redux 读取数据 console.log('%c[DynamicNewsCard] 从 Redux 读取数据', 'color: #3B82F6; font-weight: bold;', { currentMode, - 'verticalData.data?.length': verticalData.data?.length || 0, + 'verticalData.data type': typeof verticalData.data, + 'verticalData.data keys': verticalData.data ? Object.keys(verticalData.data) : [], 'verticalData.total': verticalData.total, - 'verticalData.cachedCount': verticalData.cachedCount, + 'verticalData.cachedPageCount': verticalData.cachedPageCount, 'verticalData.loading': verticalData.loading, 'fourRowData.data?.length': fourRowData.data?.length || 0, 'fourRowData.total': fourRowData.total, }); - // 根据模式选择数据源(添加默认值避免解构失败) + // 根据模式选择数据源 + // 纵向模式:data 是页码映射 { 1: [...], 2: [...] } + // 平铺模式:data 是数组 [...] + const modeData = currentMode === 'four-row' ? fourRowData : verticalData; const { - data: allCachedEvents = [], + data = currentMode === 'vertical' ? {} : [], // 纵向是对象,平铺是数组 loading = false, error = null, - total = 0, - cachedCount = 0 - } = currentMode === 'four-row' ? fourRowData : verticalData; + pagination, // 分页元数据 + total = 0, // 向后兼容 + cachedCount = 0, + cachedPageCount = 0 + } = modeData; + + // 传递给 usePagination 的数据 + const allCachedEventsByPage = currentMode === 'vertical' ? data : undefined; + const allCachedEvents = currentMode === 'four-row' ? data : undefined; // 🔍 调试:选择的数据源 console.log('%c[DynamicNewsCard] 选择的数据源', 'color: #3B82F6; font-weight: bold;', { mode: currentMode, - 'allCachedEvents.length': allCachedEvents.length, + 'allCachedEventsByPage': allCachedEventsByPage ? Object.keys(allCachedEventsByPage) : 'undefined', + 'allCachedEvents?.length': allCachedEvents?.length, total, cachedCount, + cachedPageCount, loading, error }); // 🔍 调试:记录每次渲染 dynamicNewsCardRenderCount++; - console.log(`%c🔍 [DynamicNewsCard] 渲染 #${dynamicNewsCardRenderCount} - mode=${currentMode}, allCachedEvents.length=${allCachedEvents.length}, total=${total}`, 'color: #FF9800; font-weight: bold; font-size: 14px;'); + console.log(`%c🔍 [DynamicNewsCard] 渲染 #${dynamicNewsCardRenderCount} - mode=${currentMode}, allCachedEvents.length=${allCachedEvents?.length || 0}, total=${total}`, 'color: #FF9800; font-weight: bold; font-size: 14px;'); // 关注按钮点击处理 const handleToggleFollow = useCallback((eventId) => { @@ -141,15 +153,16 @@ const DynamicNewsCard = forwardRef(({ totalPages, hasMore, currentPageEvents, - displayEvents, // 新增:累积显示的事件列表 - isAccumulateMode, // 新增:是否累积模式 + displayEvents, // 当前显示的事件列表 handlePageChange, handleModeToggle, - loadNextPage, // 新增:加载下一页 - loadPrevPage // 新增:加载上一页 + loadNextPage, // 加载下一页 + loadPrevPage // 加载上一页 } = usePagination({ - allCachedEvents, - total, + allCachedEventsByPage, // 纵向模式:页码映射 + allCachedEvents, // 平铺模式:数组 + pagination, // 分页元数据对象 + total, // 向后兼容 cachedCount, dispatch, toast, @@ -183,7 +196,11 @@ const DynamicNewsCard = forwardRef(({ // 初始加载 - 只在组件首次挂载且对应模式数据为空时执行 useEffect(() => { - if (!hasInitialized.current && allCachedEvents.length === 0) { + const isDataEmpty = currentMode === 'vertical' + ? Object.keys(allCachedEventsByPage || {}).length === 0 + : (allCachedEvents?.length || 0) === 0; + + if (!hasInitialized.current && isDataEmpty) { hasInitialized.current = true; dispatch(fetchDynamicNews({ mode: mode, // 传递当前模式 @@ -194,7 +211,7 @@ const DynamicNewsCard = forwardRef(({ page: PAGINATION_CONFIG.INITIAL_PAGE, // 然后覆盖 page 参数 })); } - }, [dispatch, allCachedEvents.length, mode, pageSize]); // ✅ 移除 filters 依赖,避免重复触发 + }, [dispatch, allCachedEventsByPage, allCachedEvents, currentMode, mode, pageSize]); // ✅ 移除 filters 依赖,避免重复触发 // 监听筛选条件变化 - 清空缓存并重新请求数据 useEffect(() => { @@ -231,7 +248,11 @@ const DynamicNewsCard = forwardRef(({ // 监听模式切换 - 如果新模式数据为空,请求数据 useEffect(() => { - if (hasInitialized.current && allCachedEvents.length === 0) { + const isDataEmpty = currentMode === 'vertical' + ? Object.keys(allCachedEventsByPage || {}).length === 0 + : (allCachedEvents?.length || 0) === 0; + + if (hasInitialized.current && isDataEmpty) { console.log(`%c🔄 [模式切换] ${mode} 模式数据为空,开始加载`, 'color: #8B5CF6; font-weight: bold;'); dispatch(fetchDynamicNews({ mode: mode, @@ -309,11 +330,10 @@ const DynamicNewsCard = forwardRef(({ {currentPageEvents && currentPageEvents.length > 0 ? ( { +export const usePagination = ({ + allCachedEventsByPage, // 纵向模式:页码映射 + allCachedEvents, // 平铺模式:数组 + pagination, // 分页元数据对象 + total, // 向后兼容 + cachedCount, + dispatch, + toast, + filters = {} +}) => { // 本地状态 const [currentPage, setCurrentPage] = useState(PAGINATION_CONFIG.INITIAL_PAGE); const [loadingPage, setLoadingPage] = useState(null); const [mode, setMode] = useState(DEFAULT_MODE); - // 累积显示的事件列表(用于四排模式的无限滚动) - const [accumulatedEvents, setAccumulatedEvents] = useState([]); - // 根据模式决定每页显示数量 const pageSize = (() => { switch (mode) { @@ -43,92 +51,55 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t } })(); - // 计算总页数(基于服务端总数据量) - const totalPages = Math.ceil(total / pageSize) || 1; + // 【优化】优先使用后端返回的 total_pages,避免前端重复计算 + // 向后兼容:如果没有 pagination 对象,则使用 total 计算 + const totalPages = pagination?.total_pages || Math.ceil((pagination?.total || total || 0) / pageSize) || 1; - // 检查是否还有更多数据 - const hasMore = cachedCount < total; + // 检查是否还有更多数据(使用页码判断,不受去重影响) + const hasMore = currentPage < totalPages; - // 判断是否使用累积模式(四排模式 + 纵向模式) - const isAccumulateMode = mode === DISPLAY_MODES.FOUR_ROW || mode === DISPLAY_MODES.VERTICAL; - - // 从缓存中切片获取当前页数据(过滤 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]); - - // 当前显示的事件列表(累积模式 vs 分页模式) - const displayEvents = useMemo(() => { - if (isAccumulateMode) { - // 四排模式:累积显示所有已加载的事件 - return accumulatedEvents; + 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; } - }, [isAccumulateMode, accumulatedEvents, currentPageEvents]); - - /** - * 子函数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); - // 修复:确保 expectedCount 不为负数 - // - 当 total = 0 时,expectedCount = pageSize,强制发起请求 - // - 当 total - targetPageStartIndex < 0 时,expectedCount = 0 - const expectedCount = total === 0 ? pageSize : Math.max(0, 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]); - - // 已删除: calculatePreloadRange(不再需要预加载) - - // 已删除: findMissingPages(不再需要查找缺失页面) + }, [mode, allCachedEvents, currentPageEvents]); /** * 加载单个页面数据 * @param {number} targetPage - 目标页码 + * @param {boolean} clearCache - 是否清空缓存(第1页专用) * @returns {Promise} 是否加载成功 */ - const loadPage = useCallback(async (targetPage) => { + 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}`, 'color: #16A34A;'); + console.log(`%c 请求参数: page=${targetPage}, per_page=${pageSize}, mode=${mode}, clearCache=${clearCache}`, 'color: #16A34A;'); console.log(`%c 筛选条件:`, 'color: #16A34A;', filters); logger.debug('DynamicNewsCard', '开始加载页面数据', { targetPage, pageSize, mode, + clearCache, filters }); @@ -138,7 +109,7 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t page: targetPage, per_page: pageSize, pageSize, - clearCache: false, + clearCache, filters }); @@ -146,7 +117,7 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t mode: mode, // 传递 mode 参数 per_page: pageSize, pageSize: pageSize, - clearCache: false, + clearCache: clearCache, // 传递 clearCache 参数 ...filters, // 先展开筛选条件 page: targetPage, // 然后覆盖 page 参数(避免被 filters.page 覆盖) })).unwrap(); @@ -175,108 +146,94 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t } finally { setLoadingPage(null); } - }, [dispatch, pageSize, toast]); + }, [dispatch, pageSize, toast, mode, filters]); - // 翻页处理(简化版 - 无预加载) + // 翻页处理(第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 !== null) { + console.log(`%c⚠️ [翻页] 正在加载第${loadingPage}页,忽略新请求第${newPage}页`, 'color: #EAB308; font-weight: bold;'); + logger.warn('usePagination', '竞态条件:正在加载中', { loadingPage, newPage }); + return; + } + console.log(`%c🔵 [翻页逻辑] handlePageChange 开始`, 'color: #3B82F6; font-weight: bold;'); - console.log(`%c 当前页: ${currentPage}, 目标页: ${newPage}, 总页数: ${totalPages}`, 'color: #3B82F6;'); - console.log(`%c 每页大小: ${pageSize}, 缓存总数: ${allCachedEvents.length}, 服务端总数: ${total}`, 'color: #3B82F6;'); + console.log(`%c 当前页: ${currentPage}, 目标页: ${newPage}, 模式: ${mode}`, 'color: #3B82F6;'); - logger.debug('DynamicNewsCard', '开始翻页', { - currentPage, - newPage, - pageSize, - totalPages, - total, - allCachedEventsLength: allCachedEvents.length, - cachedCount - }); + // 【核心逻辑】第1页特殊处理:强制清空缓存并重新加载 + if (newPage === 1) { + console.log(`%c🔄 [第1页] 清空缓存并重新加载`, 'color: #8B5CF6; font-weight: bold;'); + logger.info('usePagination', '第1页:强制刷新', { mode }); - // 检查目标页缓存状态(统一处理,包括第一页) - const { isTargetPageCached, targetPageInfo } = checkTargetPageCache(newPage); + const success = await loadPage(newPage, true); // clearCache = true - console.log(`%c🟡 [缓存检查] 目标页${newPage}缓存状态`, 'color: #EAB308; font-weight: bold;'); - console.log(`%c 是否已缓存: ${isTargetPageCached ? '✅ 是' : '❌ 否'}`, `color: ${isTargetPageCached ? '#16A34A' : '#DC2626'};`); - console.log(`%c 索引范围: ${targetPageInfo.startIndex}-${targetPageInfo.endIndex}`, 'color: #EAB308;'); - console.log(`%c 实际数量: ${targetPageInfo.validCount}, 期望数量: ${targetPageInfo.expectedCount}`, 'color: #EAB308;'); - - if (isTargetPageCached) { - // 目标页已缓存,直接切换 - console.log(`%c🟡 [缓存] 目标页已缓存,直接切换到第${newPage}页`, 'color: #16A34A; font-weight: bold;'); - logger.debug('DynamicNewsCard', '目标页已缓存,直接切换', { newPage }); - setCurrentPage(newPage); - } else { - // 目标页未缓存,显示 loading 并加载数据 - console.log(`%c🟡 [缓存] 目标页未缓存,需要加载第${newPage}页数据`, 'color: #DC2626; font-weight: bold;'); - logger.debug('DynamicNewsCard', '目标页未缓存,加载数据', { newPage }); - const success = await loadPage(newPage); - - // 加载成功后切换页面 if (success) { - console.log(`%c🟢 [加载成功] 切换到第${newPage}页`, 'color: #16A34A; font-weight: bold;'); + setCurrentPage(newPage); + } + 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;'); setCurrentPage(newPage); } else { - console.log(`%c❌ [加载失败] 未能切换到第${newPage}页`, 'color: #DC2626; font-weight: bold;'); + console.log(`%c❌ [缓存] 第${newPage}页未缓存,加载数据`, 'color: #DC2626; font-weight: bold;'); + const success = await loadPage(newPage, false); // clearCache = false + + if (success) { + setCurrentPage(newPage); + } } - } - }, [ - currentPage, - pageSize, - totalPages, - total, - allCachedEvents.length, - cachedCount, - checkTargetPageCache, - loadPage, - dispatch, - toast - ]); - - // 更新累积列表(四排模式专用) - useEffect(() => { - if (isAccumulateMode) { - // 计算已加载的所有事件(从第1页到当前页) - const startIndex = 0; - const endIndex = currentPage * pageSize; - const accumulated = allCachedEvents.slice(startIndex, endIndex).filter(e => e !== null); - - logger.debug('DynamicNewsCard', '更新累积事件列表', { - currentPage, - pageSize, - startIndex, - endIndex, - accumulatedLength: accumulated.length - }); - - setAccumulatedEvents(accumulated); } else { - // 非累积模式时清空累积列表 - if (accumulatedEvents.length > 0) { - setAccumulatedEvents([]); + // 平铺模式:直接加载新页(追加模式,clearCache=false) + console.log(`%c🟡 [平铺模式] 加载第${newPage}页`, 'color: #EAB308; font-weight: bold;'); + const success = await loadPage(newPage, false); // clearCache = false + + if (success) { + setCurrentPage(newPage); } } - }, [isAccumulateMode, currentPage, pageSize, allCachedEvents]); + }, [mode, currentPage, totalPages, loadingPage, allCachedEventsByPage, loadPage]); + // 加载下一页(用于无限滚动) const loadNextPage = useCallback(async () => { - // 修复:使用 hasMore 判断而不是 currentPage >= totalPages - // 原因:去重后 cachedCount 可能小于 total,但 currentPage 已达到 totalPages + // 使用 hasMore 判断(基于 currentPage < totalPages) if (!hasMore || loadingPage !== null) { logger.debug('DynamicNewsCard', '无法加载下一页', { currentPage, totalPages, hasMore, - cachedCount, - total, loadingPage, - reason: !hasMore ? '已加载全部数据 (cachedCount >= total)' : '正在加载中' + reason: !hasMore ? '已加载全部数据 (currentPage >= totalPages)' : '正在加载中' }); return Promise.resolve(false); // 没有更多数据或正在加载 } const nextPage = currentPage + 1; - logger.debug('DynamicNewsCard', '懒加载:加载下一页', { currentPage, nextPage, hasMore, cachedCount, total }); + logger.debug('DynamicNewsCard', '懒加载:加载下一页', { currentPage, nextPage, hasMore, totalPages }); try { await handlePageChange(nextPage); @@ -285,7 +242,7 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t logger.error('DynamicNewsCard', '懒加载失败', error, { nextPage }); return false; } - }, [currentPage, totalPages, hasMore, cachedCount, total, loadingPage, handlePageChange]); + }, [currentPage, totalPages, hasMore, loadingPage, handlePageChange]); // 加载上一页(用于双向无限滚动) const loadPrevPage = useCallback(async () => { @@ -328,13 +285,12 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t totalPages, hasMore, currentPageEvents, - displayEvents, // 新增:当前显示的事件列表(累积或分页) - isAccumulateMode, // 新增:是否累积模式 + displayEvents, // 当前显示的事件列表 // 方法 handlePageChange, handleModeToggle, - loadNextPage, // 新增:加载下一页(用于无限滚动) - loadPrevPage // 新增:加载上一页(用于双向无限滚动) + loadNextPage, // 加载下一页(用于无限滚动) + loadPrevPage // 加载上一页(用于双向无限滚动) }; }; diff --git a/src/views/Community/index.js b/src/views/Community/index.js index 7859634e..ed95f77b 100644 --- a/src/views/Community/index.js +++ b/src/views/Community/index.js @@ -187,7 +187,7 @@ const Community = () => { {/* 事件弹窗 */} - setSelectedEvent(null), @@ -199,7 +199,7 @@ const Community = () => { event: selectedEventForStock, onClose: () => setSelectedEventForStock(null) }} - /> + /> */} ); };