fix: 修复分页、筛选和模式切换相关问题

主要修复:
1. 修复模式切换时 per_page 参数错误
   - 在 useEffect 内直接根据 mode 计算 per_page
   - 避免使用可能过时的 pageSize prop

2. 修复 DISPLAY_MODES 未定义错误
   - 在 DynamicNewsCard.js 中导入 DISPLAY_MODES 常量

3. 添加空状态显示
   - VerticalModeLayout 添加无数据时的友好提示
   - 显示图标和提示文字,引导用户调整筛选条件

4. 修复无限请求循环问题
   - 移除模式切换 useEffect 中的 filters 依赖
   - 避免筛选和模式切换 useEffect 互相触发

5. 修复筛选参数传递问题
   - usePagination 使用 useRef 存储最新 filters
   - 避免 useCallback 闭包捕获旧值
   - 修复时间筛选参数丢失问题

6. 修复分页竞态条件
   - 允许用户在加载时切换到不同页面
   - 只阻止相同页面的重复请求

涉及文件:
- src/views/Community/components/DynamicNewsCard.js
- src/views/Community/components/DynamicNewsCard/VerticalModeLayout.js
- src/views/Community/components/DynamicNewsCard/hooks/usePagination.js
- src/views/Community/hooks/useEventFilters.js
- src/store/slices/communityDataSlice.js
- src/views/Community/components/UnifiedSearchBox.js

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-11-06 17:39:03 +08:00
parent 8799964961
commit 319a78d34c
6 changed files with 429 additions and 172 deletions

View File

@@ -1,8 +1,8 @@
// src/views/Community/components/DynamicNewsCard/hooks/usePagination.js
// 分页逻辑自定义 Hook
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
import { fetchDynamicNews } from '../../../../../store/slices/communityDataSlice';
import { useState, useMemo, useCallback, useRef } from 'react';
import { fetchDynamicNews, updatePaginationPage } from '../../../../../store/slices/communityDataSlice';
import { logger } from '../../../../../utils/logger';
import {
PAGINATION_CONFIG,
@@ -16,12 +16,13 @@ import {
* @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 }
* @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 = ({
@@ -32,12 +33,20 @@ export const usePagination = ({
cachedCount,
dispatch,
toast,
filters = {}
filters = {},
initialMode // 初始显示模式
}) => {
// 本地状态
const [currentPage, setCurrentPage] = useState(PAGINATION_CONFIG.INITIAL_PAGE);
const [loadingPage, setLoadingPage] = useState(null);
const [mode, setMode] = useState(DEFAULT_MODE);
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 = (() => {
@@ -93,14 +102,14 @@ export const usePagination = ({
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;', filters);
console.log(`%c 筛选条件:`, 'color: #16A34A;', filtersRef.current);
logger.debug('DynamicNewsCard', '开始加载页面数据', {
targetPage,
pageSize,
mode,
clearCache,
filters
filters: filtersRef.current
});
// 🔍 调试dispatch 前
@@ -110,7 +119,7 @@ export const usePagination = ({
per_page: pageSize,
pageSize,
clearCache,
filters
filters: filtersRef.current
});
const result = await dispatch(fetchDynamicNews({
@@ -118,7 +127,7 @@ export const usePagination = ({
per_page: pageSize,
pageSize: pageSize,
clearCache: clearCache, // 传递 clearCache 参数
...filters, // 先展开筛选条件
...filtersRef.current, // 从 ref 读取最新筛选条件
page: targetPage, // 然后覆盖 page 参数(避免被 filters.page 覆盖)
})).unwrap();
@@ -146,7 +155,7 @@ export const usePagination = ({
} finally {
setLoadingPage(null);
}
}, [dispatch, pageSize, toast, mode, filters]);
}, [dispatch, pageSize, toast, mode]); // 移除 filters 依赖,使用 filtersRef 读取最新值
// 翻页处理第1页强制刷新 + 其他页缓存)
const handlePageChange = useCallback(async (newPage) => {
@@ -164,13 +173,20 @@ export const usePagination = ({
return;
}
// 边界检查 3: 防止竞态条件 - 如果正在加载其他页面,忽略新请求
if (loadingPage !== null) {
console.log(`%c⚠ [翻页] 正在加载${loadingPage},忽略新请求第${newPage}`, 'color: #EAB308; font-weight: bold;');
logger.warn('usePagination', '竞态条件:正在加载', { loadingPage, 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;');
@@ -179,11 +195,8 @@ export const usePagination = ({
console.log(`%c🔄 [第1页] 清空缓存并重新加载`, 'color: #8B5CF6; font-weight: bold;');
logger.info('usePagination', '第1页强制刷新', { mode });
const success = await loadPage(newPage, true); // clearCache = true
if (success) {
setCurrentPage(newPage);
}
// clearCache = trueAPI 会更新 Redux pagination.current_page
await loadPage(newPage, true);
return;
}
@@ -197,23 +210,18 @@ export const usePagination = ({
if (isPageCached) {
console.log(`%c✅ [缓存] 第${newPage}页已缓存,直接切换`, 'color: #16A34A; font-weight: bold;');
setCurrentPage(newPage);
// 使用缓存数据,同步更新 Redux pagination.current_page
dispatch(updatePaginationPage({ mode, page: newPage }));
} else {
console.log(`%c❌ [缓存] 第${newPage}页未缓存,加载数据`, 'color: #DC2626; font-weight: bold;');
const success = await loadPage(newPage, false); // clearCache = false
if (success) {
setCurrentPage(newPage);
}
// clearCache = falseAPI 会更新 Redux pagination.current_page
await loadPage(newPage, false);
}
} else {
// 平铺模式直接加载新页追加模式clearCache=false
console.log(`%c🟡 [平铺模式] 加载第${newPage}`, 'color: #EAB308; font-weight: bold;');
const success = await loadPage(newPage, false); // clearCache = false
if (success) {
setCurrentPage(newPage);
}
// clearCache = falseAPI 会更新 Redux pagination.current_page
await loadPage(newPage, false);
}
}, [mode, currentPage, totalPages, loadingPage, allCachedEventsByPage, loadPage]);
@@ -272,8 +280,8 @@ export const usePagination = ({
if (newMode === mode) return;
setMode(newMode);
setCurrentPage(PAGINATION_CONFIG.INITIAL_PAGE);
// pageSize 会根据 mode 自动重新计算(第35-44行)
// currentPage 由 Redux pagination.current_page 派生,会在下次请求时自动更新
// pageSize 会根据 mode 自动重新计算(第46-56行)
}, [mode]);
return {