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:
@@ -182,8 +182,12 @@ export const fetchDynamicNews = createAsyncThunk(
|
||||
sort = 'new',
|
||||
importance,
|
||||
q,
|
||||
date_range,
|
||||
industry_code
|
||||
date_range, // 兼容旧格式(已废弃)
|
||||
industry_code,
|
||||
// 时间筛选参数(从 TradingTimeFilter 传递)
|
||||
start_date,
|
||||
end_date,
|
||||
recent_days
|
||||
} = {}, { rejectWithValue }) => {
|
||||
try {
|
||||
// 【动态计算 per_page】根据 mode 自动选择合适的每页大小
|
||||
@@ -197,8 +201,12 @@ export const fetchDynamicNews = createAsyncThunk(
|
||||
if (sort) filters.sort = sort;
|
||||
if (importance && importance !== 'all') filters.importance = importance;
|
||||
if (q) filters.q = q;
|
||||
if (date_range) filters.date_range = date_range;
|
||||
if (date_range) filters.date_range = date_range; // 兼容旧格式
|
||||
if (industry_code) filters.industry_code = industry_code;
|
||||
// 时间筛选参数
|
||||
if (start_date) filters.start_date = start_date;
|
||||
if (end_date) filters.end_date = end_date;
|
||||
if (recent_days) filters.recent_days = recent_days;
|
||||
|
||||
logger.debug('CommunityData', '开始获取动态新闻', {
|
||||
mode,
|
||||
@@ -443,6 +451,17 @@ const communityDataSlice = createSlice({
|
||||
const { eventId, isFollowing, followerCount } = action.payload;
|
||||
state.eventFollowStatus[eventId] = { isFollowing, followerCount };
|
||||
logger.debug('CommunityData', '设置事件关注状态', { eventId, isFollowing, followerCount });
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新分页页码(用于缓存场景,无需 API 请求)
|
||||
* @param {Object} action.payload - { mode, page }
|
||||
*/
|
||||
updatePaginationPage: (state, action) => {
|
||||
const { mode, page } = action.payload;
|
||||
const paginationKey = mode === 'four-row' ? 'fourRowPagination' : 'verticalPagination';
|
||||
state[paginationKey].current_page = page;
|
||||
logger.debug('CommunityData', '同步更新分页页码(缓存场景)', { mode, page });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -603,7 +622,7 @@ const communityDataSlice = createSlice({
|
||||
|
||||
// ==================== 导出 ====================
|
||||
|
||||
export const { clearCache, clearSpecificCache, preloadData, setEventFollowStatus } = communityDataSlice.actions;
|
||||
export const { clearCache, clearSpecificCache, preloadData, setEventFollowStatus, updatePaginationPage } = communityDataSlice.actions;
|
||||
|
||||
// 基础选择器(Selectors)
|
||||
export const selectPopularKeywords = (state) => state.communityData.popularKeywords;
|
||||
|
||||
@@ -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>
|
||||
|
||||
{/* 四排模式详情弹窗 - 未打开时不渲染 */}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// 纵向分栏模式布局组件
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, IconButton, Tooltip, VStack, Flex } from '@chakra-ui/react';
|
||||
import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons';
|
||||
import { Box, IconButton, Tooltip, VStack, Flex, Center, Text } from '@chakra-ui/react';
|
||||
import { ViewIcon, ViewOffIcon, InfoIcon } from '@chakra-ui/icons';
|
||||
import HorizontalDynamicNewsEventCard from '../EventCard/HorizontalDynamicNewsEventCard';
|
||||
import EventDetailScrollPanel from './EventDetailScrollPanel';
|
||||
|
||||
@@ -94,26 +94,41 @@ const VerticalModeLayout = ({
|
||||
}}
|
||||
>
|
||||
{/* 事件列表 */}
|
||||
<VStack
|
||||
spacing={2}
|
||||
align="stretch"
|
||||
p={2}
|
||||
>
|
||||
{events.map((event) => (
|
||||
<HorizontalDynamicNewsEventCard
|
||||
key={event.id}
|
||||
event={event}
|
||||
isSelected={selectedEvent?.id === event.id}
|
||||
onEventClick={() => onEventSelect(event)}
|
||||
isFollowing={eventFollowStatus[event.id]?.isFollowing}
|
||||
followerCount={eventFollowStatus[event.id]?.followerCount}
|
||||
onToggleFollow={onToggleFollow}
|
||||
timelineStyle={getTimelineBoxStyle()}
|
||||
borderColor={borderColor}
|
||||
indicatorSize={layoutMode === 'detail' ? 'default' : 'comfortable'}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
{events && events.length > 0 ? (
|
||||
<VStack
|
||||
spacing={2}
|
||||
align="stretch"
|
||||
p={2}
|
||||
>
|
||||
{events.map((event) => (
|
||||
<HorizontalDynamicNewsEventCard
|
||||
key={event.id}
|
||||
event={event}
|
||||
isSelected={selectedEvent?.id === event.id}
|
||||
onEventClick={() => onEventSelect(event)}
|
||||
isFollowing={eventFollowStatus[event.id]?.isFollowing}
|
||||
followerCount={eventFollowStatus[event.id]?.followerCount}
|
||||
onToggleFollow={onToggleFollow}
|
||||
timelineStyle={getTimelineBoxStyle()}
|
||||
borderColor={borderColor}
|
||||
indicatorSize={layoutMode === 'detail' ? 'default' : 'comfortable'}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
) : (
|
||||
/* 空状态 */
|
||||
<Center h="100%" minH="400px">
|
||||
<VStack spacing={4}>
|
||||
<InfoIcon w={12} h={12} color="gray.400" />
|
||||
<Text fontSize="lg" color="gray.500" textAlign="center">
|
||||
当前筛选条件下暂无数据
|
||||
</Text>
|
||||
<Text fontSize="sm" color="gray.400" textAlign="center">
|
||||
请尝试调整筛选条件
|
||||
</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 右侧:事件详情 - 独立滚动 */}
|
||||
|
||||
@@ -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 = true:API 会更新 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 = false:API 会更新 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 = false:API 会更新 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 {
|
||||
|
||||
@@ -22,7 +22,9 @@ const UnifiedSearchBox = ({
|
||||
onSearch,
|
||||
onSearchFocus,
|
||||
popularKeywords = [],
|
||||
filters = {}
|
||||
filters = {},
|
||||
mode, // 显示模式(如:vertical, horizontal 等)
|
||||
pageSize // 每页显示数量
|
||||
}) => {
|
||||
|
||||
// 其他状态
|
||||
@@ -145,7 +147,8 @@ const UnifiedSearchBox = ({
|
||||
}
|
||||
|
||||
// ✅ 初始化行业分类(需要 industryData 加载完成)
|
||||
if (filters.industry_code && industryData && industryData.length > 0) {
|
||||
// ⚠️ 只在 industryValue 为空时才从 filters 初始化,避免用户选择后被覆盖
|
||||
if (filters.industry_code && industryData && industryData.length > 0 && (!industryValue || industryValue.length === 0)) {
|
||||
const path = findIndustryPath(filters.industry_code, industryData);
|
||||
if (path) {
|
||||
setIndustryValue(path);
|
||||
@@ -154,6 +157,10 @@ const UnifiedSearchBox = ({
|
||||
path
|
||||
});
|
||||
}
|
||||
} else if (!filters.industry_code && industryValue && industryValue.length > 0) {
|
||||
// 如果 filters 中没有行业代码,但本地有值,清空本地值
|
||||
setIndustryValue([]);
|
||||
logger.debug('UnifiedSearchBox', '清空行业分类(filters中无值)');
|
||||
}
|
||||
|
||||
// ✅ 同步 filters.q 到输入框显示值
|
||||
@@ -163,7 +170,54 @@ const UnifiedSearchBox = ({
|
||||
// 如果 filters 中没有搜索关键词,清空输入框
|
||||
setInputValue('');
|
||||
}
|
||||
}, [filters.sort, filters.importance, filters.industry_code, filters.q, industryData, findIndustryPath]);
|
||||
|
||||
// ✅ 初始化时间筛选(从 filters 中恢复)
|
||||
// ⚠️ 只在 tradingTimeRange 为空时才从 filters 初始化,避免用户选择后被覆盖
|
||||
const hasTimeInFilters = filters.start_date || filters.end_date || filters.recent_days;
|
||||
|
||||
if (hasTimeInFilters && (!tradingTimeRange || !tradingTimeRange.key)) {
|
||||
// 根据参数推断按钮 key
|
||||
let inferredKey = 'custom';
|
||||
let inferredLabel = '';
|
||||
|
||||
if (filters.recent_days) {
|
||||
// 推断是否是预设按钮
|
||||
if (filters.recent_days === '7') {
|
||||
inferredKey = 'week';
|
||||
inferredLabel = '近一周';
|
||||
} else if (filters.recent_days === '30') {
|
||||
inferredKey = 'month';
|
||||
inferredLabel = '近一月';
|
||||
} else {
|
||||
inferredLabel = `近${filters.recent_days}天`;
|
||||
}
|
||||
} else if (filters.start_date && filters.end_date) {
|
||||
inferredLabel = `${dayjs(filters.start_date).format('MM-DD HH:mm')} - ${dayjs(filters.end_date).format('MM-DD HH:mm')}`;
|
||||
}
|
||||
|
||||
// 从 filters 重建 tradingTimeRange 状态
|
||||
const timeRange = {
|
||||
start_date: filters.start_date || '',
|
||||
end_date: filters.end_date || '',
|
||||
recent_days: filters.recent_days || '',
|
||||
label: inferredLabel,
|
||||
key: inferredKey
|
||||
};
|
||||
setTradingTimeRange(timeRange);
|
||||
logger.debug('UnifiedSearchBox', '初始化时间筛选', {
|
||||
filters_time: {
|
||||
start_date: filters.start_date,
|
||||
end_date: filters.end_date,
|
||||
recent_days: filters.recent_days
|
||||
},
|
||||
tradingTimeRange: timeRange
|
||||
});
|
||||
} else if (!hasTimeInFilters && tradingTimeRange) {
|
||||
// 如果 filters 中没有时间参数,但本地有值,清空本地值
|
||||
setTradingTimeRange(null);
|
||||
logger.debug('UnifiedSearchBox', '清空时间筛选(filters中无值)');
|
||||
}
|
||||
}, [filters.sort, filters.importance, filters.industry_code, filters.q, filters.start_date, filters.end_date, filters.recent_days, industryData, findIndustryPath, industryValue, tradingTimeRange]);
|
||||
|
||||
// AutoComplete 搜索股票(模糊匹配 code 或 name)
|
||||
const handleSearch = (value) => {
|
||||
@@ -242,59 +296,45 @@ const UnifiedSearchBox = ({
|
||||
triggerSearch(params);
|
||||
};
|
||||
|
||||
// ✅ 排序变化(使用防抖)
|
||||
// ✅ 排序变化(立即触发搜索)
|
||||
const handleSortChange = (value) => {
|
||||
logger.debug('UnifiedSearchBox', '【1/5】排序值改变', {
|
||||
logger.debug('UnifiedSearchBox', '排序值改变', {
|
||||
oldValue: sort,
|
||||
newValue: value
|
||||
});
|
||||
setSort(value);
|
||||
|
||||
// ⚠️ 注意:setState是异步的,此时sort仍是旧值
|
||||
logger.debug('UnifiedSearchBox', '【2/5】调用buildFilterParams前的状态', {
|
||||
sort: sort, // 旧值
|
||||
importance: importance,
|
||||
dateRange: dateRange,
|
||||
industryValue: industryValue
|
||||
});
|
||||
|
||||
// 使用防抖搜索
|
||||
const params = buildFilterParams({ sort: value });
|
||||
logger.debug('UnifiedSearchBox', '【3/5】buildFilterParams返回的参数', params);
|
||||
|
||||
// 取消之前的防抖搜索
|
||||
if (debouncedSearchRef.current) {
|
||||
logger.debug('UnifiedSearchBox', '【4/5】调用防抖函数(300ms延迟)');
|
||||
debouncedSearchRef.current(params);
|
||||
debouncedSearchRef.current.cancel();
|
||||
}
|
||||
|
||||
// 立即触发搜索
|
||||
const params = buildFilterParams({ sort: value });
|
||||
logger.debug('UnifiedSearchBox', '排序改变,立即触发搜索', params);
|
||||
triggerSearch(params);
|
||||
};
|
||||
|
||||
// ✅ 行业分类变化(使用防抖)
|
||||
// ✅ 行业分类变化(立即触发搜索)
|
||||
const handleIndustryChange = (value) => {
|
||||
logger.debug('UnifiedSearchBox', '【1/5】行业分类值改变', {
|
||||
logger.debug('UnifiedSearchBox', '行业分类值改变', {
|
||||
oldValue: industryValue,
|
||||
newValue: value
|
||||
});
|
||||
setIndustryValue(value);
|
||||
|
||||
// ⚠️ 注意:setState是异步的,此时industryValue仍是旧值
|
||||
logger.debug('UnifiedSearchBox', '【2/5】调用buildFilterParams前的状态', {
|
||||
industryValue: industryValue, // 旧值
|
||||
sort: sort,
|
||||
importance: importance,
|
||||
dateRange: dateRange
|
||||
});
|
||||
|
||||
// 使用防抖搜索 (需要从新值推导参数)
|
||||
const params = {
|
||||
...buildFilterParams(),
|
||||
industry_code: value?.[value.length - 1] || ''
|
||||
};
|
||||
logger.debug('UnifiedSearchBox', '【3/5】buildFilterParams返回的参数', params);
|
||||
|
||||
// 取消之前的防抖搜索
|
||||
if (debouncedSearchRef.current) {
|
||||
logger.debug('UnifiedSearchBox', '【4/5】调用防抖函数(300ms延迟)');
|
||||
debouncedSearchRef.current(params);
|
||||
debouncedSearchRef.current.cancel();
|
||||
}
|
||||
|
||||
// 立即触发搜索
|
||||
const params = buildFilterParams({
|
||||
industry_code: value?.[value.length - 1] || ''
|
||||
});
|
||||
logger.debug('UnifiedSearchBox', '行业改变,立即触发搜索', params);
|
||||
|
||||
triggerSearch(params);
|
||||
};
|
||||
|
||||
// ✅ 热门概念点击处理(立即搜索,不使用防抖) - 更新输入框并触发搜索
|
||||
@@ -350,7 +390,7 @@ const UnifiedSearchBox = ({
|
||||
setTradingTimeRange({ ...params, label, key });
|
||||
|
||||
// 立即触发搜索
|
||||
const searchParams = buildFilterParams(params);
|
||||
const searchParams = buildFilterParams({ ...params, mode });
|
||||
logger.debug('UnifiedSearchBox', '交易时段筛选变化,立即触发搜索', {
|
||||
timeConfig,
|
||||
params: searchParams
|
||||
@@ -392,7 +432,9 @@ const UnifiedSearchBox = ({
|
||||
sort,
|
||||
importance,
|
||||
industryValue,
|
||||
'filters.q': filters.q
|
||||
'filters.q': filters.q,
|
||||
mode,
|
||||
pageSize
|
||||
}
|
||||
});
|
||||
|
||||
@@ -421,7 +463,7 @@ const UnifiedSearchBox = ({
|
||||
// 基础参数(overrides 优先级高于本地状态)
|
||||
sort: actualSort,
|
||||
importance: importanceValue,
|
||||
page: 1,
|
||||
|
||||
|
||||
// 搜索参数: 统一使用 q 参数进行搜索(话题/股票/关键词)
|
||||
q: (overrides.q ?? filters.q) ?? '',
|
||||
@@ -434,17 +476,30 @@ const UnifiedSearchBox = ({
|
||||
recent_days: overrides.recent_days ?? (tradingTimeRange?.recent_days || ''),
|
||||
|
||||
// 最终 overrides 具有最高优先级
|
||||
...overrides
|
||||
...overrides,
|
||||
page: 1,
|
||||
per_page: overrides.mode === 'four-row' ? 30: 10
|
||||
};
|
||||
|
||||
// 删除可能来自 overrides 的旧 per_page 值(将由 pageSize 重新设置)
|
||||
delete result.per_page;
|
||||
|
||||
// 添加 return_type 参数(如果需要)
|
||||
if (returnType) {
|
||||
result.return_type = returnType;
|
||||
}
|
||||
|
||||
// 添加 mode 和 per_page 参数(如果提供了的话)
|
||||
if (mode !== undefined && mode !== null) {
|
||||
result.mode = mode;
|
||||
}
|
||||
if (pageSize !== undefined && pageSize !== null) {
|
||||
result.per_page = pageSize; // 后端实际使用的参数
|
||||
}
|
||||
|
||||
logger.debug('UnifiedSearchBox', '🔧 buildFilterParams - 输出结果', result);
|
||||
return result;
|
||||
}, [sort, importance, filters.q, industryValue, tradingTimeRange]);
|
||||
}, [sort, importance, filters.q, industryValue, tradingTimeRange, mode, pageSize]);
|
||||
|
||||
// ✅ 重置筛选 - 清空所有筛选器并触发搜索
|
||||
const handleReset = () => {
|
||||
@@ -578,12 +633,12 @@ const UnifiedSearchBox = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div style={{padding: '8px'}}>
|
||||
{/* 第三行:行业 + 重要性 + 排序 */}
|
||||
<Space style={{ width: '100%', justifyContent: 'space-between' }} size="middle">
|
||||
{/* 左侧:筛选器组 */}
|
||||
<Space size="middle" wrap>
|
||||
<span style={{ fontSize: 14, color: '#666', fontWeight: 'bold' }}>筛选:</span>
|
||||
<Space size="small" wrap>
|
||||
<span style={{ fontSize: 12, color: '#666', fontWeight: 'bold' }}>筛选:</span>
|
||||
{/* 行业分类 */}
|
||||
<Cascader
|
||||
value={industryValue}
|
||||
@@ -602,19 +657,19 @@ const UnifiedSearchBox = ({
|
||||
expandTrigger="hover"
|
||||
displayRender={(labels) => labels.join(' > ')}
|
||||
disabled={industryLoading}
|
||||
style={{ width: 200 }}
|
||||
size="middle"
|
||||
style={{ width: 160 }}
|
||||
size="small"
|
||||
/>
|
||||
|
||||
{/* 重要性 */}
|
||||
<Space size="small">
|
||||
<span style={{ fontSize: 14, color: '#666' }}>重要性:</span>
|
||||
<span style={{ fontSize: 12, color: '#666' }}>重要性:</span>
|
||||
<AntSelect
|
||||
mode="multiple"
|
||||
value={importance}
|
||||
onChange={handleImportanceChange}
|
||||
style={{ width: 150 }}
|
||||
size="middle"
|
||||
style={{ width: 120 }}
|
||||
size="small"
|
||||
placeholder="全部"
|
||||
maxTagCount={3}
|
||||
>
|
||||
@@ -626,27 +681,27 @@ const UnifiedSearchBox = ({
|
||||
</Space>
|
||||
|
||||
{/* 搜索图标(可点击) + 搜索框 */}
|
||||
<Space.Compact style={{ flex: 1, minWidth: 300 }}>
|
||||
<Space.Compact style={{ flex: 1, minWidth: 250 }}>
|
||||
<SearchOutlined
|
||||
onClick={handleMainSearch}
|
||||
style={{
|
||||
fontSize: 18,
|
||||
padding: '8px 12px',
|
||||
background: '#f5f5f5',
|
||||
fontSize: 14,
|
||||
padding: '5px 8px',
|
||||
background: '#e6f7ff',
|
||||
borderRadius: '6px 0 0 6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: '#666',
|
||||
color: '#1890ff',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = '#1890ff';
|
||||
e.currentTarget.style.background = '#e6f7ff';
|
||||
e.currentTarget.style.color = '#096dd9';
|
||||
e.currentTarget.style.background = '#bae7ff';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = '#666';
|
||||
e.currentTarget.style.background = '#f5f5f5';
|
||||
e.currentTarget.style.color = '#1890ff';
|
||||
e.currentTarget.style.background = '#e6f7ff';
|
||||
}}
|
||||
/>
|
||||
<AutoComplete
|
||||
@@ -663,7 +718,7 @@ const UnifiedSearchBox = ({
|
||||
}
|
||||
}}
|
||||
style={{ flex: 1 }}
|
||||
size="middle"
|
||||
size="small"
|
||||
notFoundContent={inputValue && stockOptions.length === 0 ? "未找到匹配的股票" : null}
|
||||
/>
|
||||
</Space.Compact>
|
||||
@@ -672,14 +727,14 @@ const UnifiedSearchBox = ({
|
||||
<Button
|
||||
icon={<CloseCircleOutlined />}
|
||||
onClick={handleReset}
|
||||
size="middle"
|
||||
size="small"
|
||||
style={{
|
||||
borderRadius: 6,
|
||||
border: '1px solid #d9d9d9',
|
||||
backgroundColor: '#fff',
|
||||
color: '#666',
|
||||
fontWeight: 500,
|
||||
padding: '4px 12px',
|
||||
padding: '4px 10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
@@ -707,12 +762,12 @@ const UnifiedSearchBox = ({
|
||||
|
||||
{/* 右侧:排序 */}
|
||||
<Space size="small">
|
||||
<span style={{ fontSize: 14, color: '#666' }}>排序:</span>
|
||||
<span style={{ fontSize: 12, color: '#666' }}>排序:</span>
|
||||
<AntSelect
|
||||
value={sort}
|
||||
onChange={handleSortChange}
|
||||
style={{ width: 120 }}
|
||||
size="middle"
|
||||
style={{ width: 100 }}
|
||||
size="small"
|
||||
>
|
||||
<Option value="new">最新</Option>
|
||||
<Option value="hot">最热</Option>
|
||||
@@ -724,7 +779,7 @@ const UnifiedSearchBox = ({
|
||||
</Space>
|
||||
|
||||
{/* 第一行:筛选 + 时间按钮 + 搜索图标 + 搜索框 */}
|
||||
<Space wrap style={{ width: '100%', marginBottom: 12, marginTop: 8 }} size="middle">
|
||||
<Space wrap style={{ width: '100%', marginBottom: 4, marginTop: 6 }} size="middle">
|
||||
<span style={{ fontSize: 14, color: '#666', fontWeight: 'bold' }}>时间筛选:</span>
|
||||
|
||||
{/* 交易时段筛选 */}
|
||||
@@ -735,29 +790,13 @@ const UnifiedSearchBox = ({
|
||||
</Space>
|
||||
|
||||
{/* 第二行:热门概念 */}
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<div style={{ marginTop: 2 }}>
|
||||
<PopularKeywords
|
||||
keywords={popularKeywords}
|
||||
onKeywordClick={handleKeywordClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 已选条件标签 */}
|
||||
{filterTags.length > 0 && (
|
||||
<Space size={[8, 8]} wrap style={{ marginTop: 12 }}>
|
||||
{filterTags.map(tag => (
|
||||
<Tag
|
||||
key={tag.key}
|
||||
closable
|
||||
onClose={() => handleRemoveTag(tag.key)}
|
||||
color="blue"
|
||||
>
|
||||
{tag.label}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -24,9 +24,12 @@ export const useEventFilters = ({ navigate, onEventClick, eventTimelineRef } = {
|
||||
return {
|
||||
sort: searchParams.get('sort') || 'new',
|
||||
importance: searchParams.get('importance') || 'all',
|
||||
date_range: searchParams.get('date_range') || '',
|
||||
q: searchParams.get('q') || '',
|
||||
industry_code: searchParams.get('industry_code') || '',
|
||||
// 时间筛选参数(从 TradingTimeFilter 传递)
|
||||
start_date: searchParams.get('start_date') || '',
|
||||
end_date: searchParams.get('end_date') || '',
|
||||
recent_days: searchParams.get('recent_days') || '',
|
||||
page: parseInt(searchParams.get('page') || '1', 10)
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user