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:
@@ -24,7 +24,7 @@ import {
|
||||
ModalCloseButton,
|
||||
useColorModeValue,
|
||||
useToast,
|
||||
useDisclosure
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { TimeIcon } from '@chakra-ui/icons';
|
||||
import EventScrollList from './DynamicNewsCard/EventScrollList';
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
selectFourRowEventsWithLoading
|
||||
} from '../../../store/slices/communityDataSlice';
|
||||
import { usePagination } from './DynamicNewsCard/hooks/usePagination';
|
||||
import { PAGINATION_CONFIG } from './DynamicNewsCard/constants';
|
||||
import { PAGINATION_CONFIG, DISPLAY_MODES } from './DynamicNewsCard/constants';
|
||||
|
||||
// 🔍 调试:渲染计数器
|
||||
let dynamicNewsCardRenderCount = 0;
|
||||
@@ -71,11 +71,23 @@ const DynamicNewsCard = forwardRef(({
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
||||
|
||||
// 固定模式状态
|
||||
const [isFixedMode, setIsFixedMode] = useState(false);
|
||||
const [headerHeight, setHeaderHeight] = useState(0);
|
||||
const cardHeaderRef = useRef(null);
|
||||
const cardBodyRef = useRef(null);
|
||||
|
||||
// 导航栏和页脚固定高度
|
||||
const NAVBAR_HEIGHT = 64; // 主导航高度
|
||||
const SECONDARY_NAV_HEIGHT = 44; // 二级导航高度
|
||||
const FOOTER_HEIGHT = 120; // 页脚高度(预留)
|
||||
const TOTAL_NAV_HEIGHT = NAVBAR_HEIGHT + SECONDARY_NAV_HEIGHT; // 总导航高度 128px
|
||||
|
||||
// 从 Redux 读取关注状态
|
||||
const eventFollowStatus = useSelector(selectEventFollowStatus);
|
||||
|
||||
// 本地状态:模式(先初始化,后面会被 usePagination 更新)
|
||||
const [currentMode, setCurrentMode] = useState('vertical');
|
||||
// 本地状态:模式(先初始化,后面会被 usePagination 更新)
|
||||
const [currentMode, setCurrentMode] = useState('vertical');
|
||||
|
||||
// 根据当前模式从 Redux 读取对应的数据(添加默认值避免 undefined)
|
||||
const verticalData = useSelector(selectVerticalEventsWithLoading) || {};
|
||||
@@ -168,7 +180,8 @@ const DynamicNewsCard = forwardRef(({
|
||||
cachedCount,
|
||||
dispatch,
|
||||
toast,
|
||||
filters // 传递筛选条件
|
||||
filters, // 传递筛选条件
|
||||
initialMode: currentMode // 传递当前显示模式
|
||||
});
|
||||
|
||||
// 同步 mode 到 currentMode
|
||||
@@ -244,7 +257,9 @@ const DynamicNewsCard = forwardRef(({
|
||||
filters.sort,
|
||||
filters.importance,
|
||||
filters.q,
|
||||
filters.date_range,
|
||||
filters.start_date, // 时间筛选参数:开始时间
|
||||
filters.end_date, // 时间筛选参数:结束时间
|
||||
filters.recent_days, // 时间筛选参数:近N天
|
||||
filters.industry_code,
|
||||
mode, // 添加 mode 到依赖
|
||||
pageSize, // 添加 pageSize 到依赖
|
||||
@@ -259,16 +274,24 @@ const DynamicNewsCard = forwardRef(({
|
||||
|
||||
if (hasInitialized.current && isDataEmpty) {
|
||||
console.log(`%c🔄 [模式切换] ${mode} 模式数据为空,开始加载`, 'color: #8B5CF6; font-weight: bold;');
|
||||
|
||||
// 🔧 根据 mode 直接计算 per_page,避免使用可能过时的 pageSize prop
|
||||
const modePageSize = mode === DISPLAY_MODES.FOUR_ROW
|
||||
? PAGINATION_CONFIG.FOUR_ROW_PAGE_SIZE // 30
|
||||
: PAGINATION_CONFIG.VERTICAL_PAGE_SIZE; // 10
|
||||
|
||||
console.log(`%c 计算的 per_page: ${modePageSize} (mode: ${mode})`, 'color: #8B5CF6;');
|
||||
|
||||
dispatch(fetchDynamicNews({
|
||||
mode: mode,
|
||||
per_page: pageSize,
|
||||
pageSize: pageSize,
|
||||
per_page: modePageSize, // 使用计算的值,不是 pageSize prop
|
||||
pageSize: modePageSize,
|
||||
clearCache: true,
|
||||
...filters, // 先展开筛选条件
|
||||
page: PAGINATION_CONFIG.INITIAL_PAGE, // 然后覆盖 page 参数
|
||||
}));
|
||||
}
|
||||
}, [mode]); // 只监听 mode 变化
|
||||
}, [mode, currentMode, allCachedEventsByPage, allCachedEvents, dispatch]); // 移除 filters 依赖,避免与筛选 useEffect 循环触发 // 添加所有依赖
|
||||
|
||||
// 自动选中逻辑 - 只在首次加载时自动选中第一个事件,翻页时不自动选中
|
||||
useEffect(() => {
|
||||
@@ -295,10 +318,149 @@ const DynamicNewsCard = forwardRef(({
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 页码切换时滚动到顶部
|
||||
const handlePageChangeWithScroll = useCallback((page) => {
|
||||
// 先切换页码
|
||||
handlePageChange(page);
|
||||
|
||||
// 延迟一帧,确保DOM更新完成后再滚动
|
||||
requestAnimationFrame(() => {
|
||||
// 查找所有标记为滚动容器的元素
|
||||
const containers = document.querySelectorAll('[data-scroll-container]');
|
||||
containers.forEach(container => {
|
||||
container.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
console.log('📜 页码切换,滚动到顶部', { containersFound: containers.length });
|
||||
});
|
||||
}, [handlePageChange]);
|
||||
|
||||
// 测量 CardHeader 高度
|
||||
useEffect(() => {
|
||||
const cardHeaderElement = cardHeaderRef.current;
|
||||
if (!cardHeaderElement) return;
|
||||
|
||||
// 测量并更新高度
|
||||
const updateHeaderHeight = () => {
|
||||
const height = cardHeaderElement.offsetHeight;
|
||||
setHeaderHeight(height);
|
||||
};
|
||||
|
||||
// 初始测量
|
||||
updateHeaderHeight();
|
||||
|
||||
// 监听窗口大小变化(响应式调整)
|
||||
window.addEventListener('resize', updateHeaderHeight);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateHeaderHeight);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 监听 CardHeader 是否到达触发点,动态切换固定模式
|
||||
useEffect(() => {
|
||||
const cardHeaderElement = cardHeaderRef.current;
|
||||
const cardBodyElement = cardBodyRef.current;
|
||||
if (!cardHeaderElement || !cardBodyElement) return;
|
||||
|
||||
let ticking = false;
|
||||
const TRIGGER_OFFSET = 100; // 提前 100px 触发
|
||||
|
||||
// 外部滚动监听:触发固定模式
|
||||
const handleExternalScroll = () => {
|
||||
// 只在非固定模式下监听外部滚动
|
||||
if (!isFixedMode && !ticking) {
|
||||
window.requestAnimationFrame(() => {
|
||||
// 获取 CardHeader 相对视口的位置
|
||||
const rect = cardHeaderElement.getBoundingClientRect();
|
||||
const elementTop = rect.top;
|
||||
|
||||
// 计算触发点:总导航高度 + 100px 偏移量
|
||||
const triggerPoint = TOTAL_NAV_HEIGHT + TRIGGER_OFFSET;
|
||||
|
||||
// 向上滑动:元素顶部到达触发点 → 激活固定模式
|
||||
if (elementTop <= triggerPoint) {
|
||||
setIsFixedMode(true);
|
||||
console.log('🔒 切换为固定全屏模式', {
|
||||
elementTop,
|
||||
triggerPoint,
|
||||
offset: TRIGGER_OFFSET
|
||||
});
|
||||
}
|
||||
|
||||
ticking = false;
|
||||
});
|
||||
|
||||
ticking = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 内部滚动监听:退出固定模式
|
||||
const handleWheel = (e) => {
|
||||
// 只在固定模式下监听内部滚动
|
||||
if (!isFixedMode) return;
|
||||
|
||||
// 检测向上滚动(deltaY < 0)
|
||||
if (e.deltaY < 0) {
|
||||
// 查找所有滚动容器
|
||||
const scrollContainers = cardBodyElement.querySelectorAll('[data-scroll-container]');
|
||||
|
||||
if (scrollContainers.length === 0) {
|
||||
// 如果没有找到标记的容器,查找所有可滚动元素
|
||||
const allScrollable = cardBodyElement.querySelectorAll('[style*="overflow"]');
|
||||
scrollContainers = allScrollable;
|
||||
}
|
||||
|
||||
// 检查是否所有滚动容器都在顶部
|
||||
const allAtTop = scrollContainers.length === 0 ||
|
||||
Array.from(scrollContainers).every(
|
||||
container => container.scrollTop === 0
|
||||
);
|
||||
|
||||
if (allAtTop) {
|
||||
setIsFixedMode(false);
|
||||
console.log('🔓 恢复正常文档流模式(内部滚动到顶部)');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 监听外部滚动
|
||||
window.addEventListener('scroll', handleExternalScroll, { passive: true });
|
||||
|
||||
// 监听内部滚轮事件(固定模式下)
|
||||
if (isFixedMode) {
|
||||
cardBodyElement.addEventListener('wheel', handleWheel, { passive: true });
|
||||
}
|
||||
|
||||
// 初次检查位置
|
||||
handleExternalScroll();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleExternalScroll);
|
||||
cardBodyElement.removeEventListener('wheel', handleWheel);
|
||||
};
|
||||
}, [isFixedMode]);
|
||||
|
||||
return (
|
||||
<Card ref={ref} {...rest} bg={cardBg} borderColor={borderColor} mb={4}>
|
||||
<Card
|
||||
ref={ref}
|
||||
{...rest}
|
||||
bg={cardBg}
|
||||
borderColor={borderColor}
|
||||
mb={4}
|
||||
>
|
||||
{/* 标题部分 */}
|
||||
<CardHeader>
|
||||
<CardHeader
|
||||
ref={cardHeaderRef}
|
||||
position={isFixedMode ? 'fixed' : 'relative'}
|
||||
top={isFixedMode ? `${TOTAL_NAV_HEIGHT}px` : 'auto'}
|
||||
left={isFixedMode ? 0 : 'auto'}
|
||||
right={isFixedMode ? 0 : 'auto'}
|
||||
maxW={isFixedMode ? 'container.xl' : '100%'}
|
||||
mx={isFixedMode ? 'auto' : 0}
|
||||
px={isFixedMode ? { base: 3, md: 4 } : undefined}
|
||||
zIndex={isFixedMode ? 999 : 1}
|
||||
bg={cardBg}
|
||||
>
|
||||
<Flex justify="space-between" align="center">
|
||||
<VStack align="start" spacing={1}>
|
||||
<Heading size="md">
|
||||
@@ -325,15 +487,34 @@ const DynamicNewsCard = forwardRef(({
|
||||
onSearchFocus={onSearchFocus}
|
||||
popularKeywords={popularKeywords}
|
||||
filters={filters}
|
||||
mode={mode}
|
||||
pageSize={pageSize}
|
||||
/>
|
||||
</Box>
|
||||
</CardHeader>
|
||||
|
||||
{/* 主体内容 */}
|
||||
<CardBody position="relative" pt={0}>
|
||||
{/* 顶部控制栏:模式切换按钮 + 分页控制器(始终显示) */}
|
||||
<Flex justify="space-between" align="center" mb={2}>
|
||||
{/* 左侧:模式切换按钮 */}
|
||||
<CardBody
|
||||
ref={cardBodyRef}
|
||||
position={isFixedMode ? 'fixed' : 'relative'}
|
||||
top={isFixedMode ? `${TOTAL_NAV_HEIGHT + headerHeight}px` : 'auto'}
|
||||
left={isFixedMode ? 0 : 'auto'}
|
||||
right={isFixedMode ? 0 : 'auto'}
|
||||
bottom={isFixedMode ? `${FOOTER_HEIGHT}px` : 'auto'}
|
||||
maxW={isFixedMode ? 'container.xl' : '100%'}
|
||||
mx={isFixedMode ? 'auto' : 0}
|
||||
h={isFixedMode ? `calc(100vh - ${TOTAL_NAV_HEIGHT + headerHeight + FOOTER_HEIGHT}px)` : 'auto'}
|
||||
px={isFixedMode ? { base: 3, md: 4 } : undefined}
|
||||
pt={4}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
overflow="hidden"
|
||||
zIndex={isFixedMode ? 1000 : 1}
|
||||
bg={cardBg}
|
||||
>
|
||||
{/* 顶部控制栏:模式切换按钮 + 筛选按钮 + 分页控制器(固定不滚动) */}
|
||||
<Flex justify="space-between" align="center" mb={2} flexShrink={0}>
|
||||
{/* 左侧:模式切换按钮 + 筛选按钮 */}
|
||||
<ModeToggleButtons mode={mode} onModeChange={handleModeToggle} />
|
||||
|
||||
{/* 右侧:分页控制器(仅在纵向模式显示) */}
|
||||
@@ -341,13 +522,13 @@ const DynamicNewsCard = forwardRef(({
|
||||
<PaginationControl
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
onPageChange={handlePageChangeWithScroll}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{/* 横向滚动事件列表 - 始终渲染 + Loading 蒙层 */}
|
||||
<Box position="relative">
|
||||
{/* 内容区域 - 撑满剩余高度 */}
|
||||
<Box flex="1" minH={0} position="relative">
|
||||
{/* Loading 蒙层 - 数据请求时显示 */}
|
||||
{loading && (
|
||||
<Box
|
||||
@@ -384,7 +565,7 @@ const DynamicNewsCard = forwardRef(({
|
||||
borderColor={borderColor}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
onPageChange={handlePageChangeWithScroll}
|
||||
loading={loadingPage !== null}
|
||||
error={error}
|
||||
mode={mode}
|
||||
@@ -393,14 +574,6 @@ const DynamicNewsCard = forwardRef(({
|
||||
hasMore={hasMore}
|
||||
/>
|
||||
</Box>
|
||||
{/* 底部:分页控制器(仅在纵向模式显示) */}
|
||||
{mode === 'vertical' && totalPages > 1 && (
|
||||
<PaginationControl
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
)}
|
||||
</CardBody>
|
||||
|
||||
{/* 四排模式详情弹窗 - 未打开时不渲染 */}
|
||||
|
||||
Reference in New Issue
Block a user