diff --git a/src/store/slices/communityDataSlice.js b/src/store/slices/communityDataSlice.js index de6bdf00..b6cf7db0 100644 --- a/src/store/slices/communityDataSlice.js +++ b/src/store/slices/communityDataSlice.js @@ -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; diff --git a/src/views/Community/components/DynamicNewsCard.js b/src/views/Community/components/DynamicNewsCard.js index 9a6e1742..085ed926 100644 --- a/src/views/Community/components/DynamicNewsCard.js +++ b/src/views/Community/components/DynamicNewsCard.js @@ -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 ( - + {/* 标题部分 */} - + @@ -325,15 +487,34 @@ const DynamicNewsCard = forwardRef(({ onSearchFocus={onSearchFocus} popularKeywords={popularKeywords} filters={filters} + mode={mode} + pageSize={pageSize} /> {/* 主体内容 */} - - {/* 顶部控制栏:模式切换按钮 + 分页控制器(始终显示) */} - - {/* 左侧:模式切换按钮 */} + + {/* 顶部控制栏:模式切换按钮 + 筛选按钮 + 分页控制器(固定不滚动) */} + + {/* 左侧:模式切换按钮 + 筛选按钮 */} {/* 右侧:分页控制器(仅在纵向模式显示) */} @@ -341,13 +522,13 @@ const DynamicNewsCard = forwardRef(({ )} - {/* 横向滚动事件列表 - 始终渲染 + Loading 蒙层 */} - + {/* 内容区域 - 撑满剩余高度 */} + {/* Loading 蒙层 - 数据请求时显示 */} {loading && ( - {/* 底部:分页控制器(仅在纵向模式显示) */} - {mode === 'vertical' && totalPages > 1 && ( - - )} {/* 四排模式详情弹窗 - 未打开时不渲染 */} diff --git a/src/views/Community/components/DynamicNewsCard/VerticalModeLayout.js b/src/views/Community/components/DynamicNewsCard/VerticalModeLayout.js index 17901529..af0c2ae2 100644 --- a/src/views/Community/components/DynamicNewsCard/VerticalModeLayout.js +++ b/src/views/Community/components/DynamicNewsCard/VerticalModeLayout.js @@ -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 = ({ }} > {/* 事件列表 */} - - {events.map((event) => ( - onEventSelect(event)} - isFollowing={eventFollowStatus[event.id]?.isFollowing} - followerCount={eventFollowStatus[event.id]?.followerCount} - onToggleFollow={onToggleFollow} - timelineStyle={getTimelineBoxStyle()} - borderColor={borderColor} - indicatorSize={layoutMode === 'detail' ? 'default' : 'comfortable'} - /> - ))} - + {events && events.length > 0 ? ( + + {events.map((event) => ( + onEventSelect(event)} + isFollowing={eventFollowStatus[event.id]?.isFollowing} + followerCount={eventFollowStatus[event.id]?.followerCount} + onToggleFollow={onToggleFollow} + timelineStyle={getTimelineBoxStyle()} + borderColor={borderColor} + indicatorSize={layoutMode === 'detail' ? 'default' : 'comfortable'} + /> + ))} + + ) : ( + /* 空状态 */ +
+ + + + 当前筛选条件下暂无数据 + + + 请尝试调整筛选条件 + + +
+ )} {/* 右侧:事件详情 - 独立滚动 */} diff --git a/src/views/Community/components/DynamicNewsCard/hooks/usePagination.js b/src/views/Community/components/DynamicNewsCard/hooks/usePagination.js index f7aca0a0..b9f1ad1c 100644 --- a/src/views/Community/components/DynamicNewsCard/hooks/usePagination.js +++ b/src/views/Community/components/DynamicNewsCard/hooks/usePagination.js @@ -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 { diff --git a/src/views/Community/components/UnifiedSearchBox.js b/src/views/Community/components/UnifiedSearchBox.js index 55fa7276..a8a57221 100644 --- a/src/views/Community/components/UnifiedSearchBox.js +++ b/src/views/Community/components/UnifiedSearchBox.js @@ -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 ( - +
{/* 第三行:行业 + 重要性 + 排序 */} {/* 左侧:筛选器组 */} - - 筛选: + + 筛选: {/* 行业分类 */} labels.join(' > ')} disabled={industryLoading} - style={{ width: 200 }} - size="middle" + style={{ width: 160 }} + size="small" /> {/* 重要性 */} - 重要性: + 重要性: @@ -626,27 +681,27 @@ const UnifiedSearchBox = ({ {/* 搜索图标(可点击) + 搜索框 */} - + { - 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'; }} /> @@ -672,14 +727,14 @@ const UnifiedSearchBox = ({