// src/views/Community/components/CompactSearchBox.js // 紧凑版搜索和筛选组件 - 优化布局 import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react'; import { Input, Cascader, Button, Space, Tag, AutoComplete, Select as AntSelect, Tooltip, Divider, Flex } from 'antd'; import { SearchOutlined, CloseCircleOutlined, StockOutlined, FilterOutlined, CalendarOutlined, SortAscendingOutlined, ReloadOutlined, ThunderboltOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import debounce from 'lodash/debounce'; import { useSelector, useDispatch } from 'react-redux'; import { fetchIndustryData, selectIndustryData, selectIndustryLoading } from '@store/slices/industrySlice'; import { loadAllStocks } from '@store/slices/stockSlice'; import { stockService } from '@services/stockService'; import { logger } from '@utils/logger'; import TradingTimeFilter from './TradingTimeFilter'; import { PROFESSIONAL_COLORS } from '@constants/professionalTheme'; import './CompactSearchBox.css'; const { Option } = AntSelect; // 排序选项常量 const SORT_OPTIONS = [ { value: 'new', label: '最新排序', mobileLabel: '最新' }, { value: 'hot', label: '最热排序', mobileLabel: '热门' }, { value: 'importance', label: '重要性排序', mobileLabel: '重要' }, { value: 'returns_avg', label: '平均收益', mobileLabel: '均收' }, { value: 'returns_week', label: '周收益', mobileLabel: '周收' }, ]; // 重要性等级常量 const IMPORTANCE_OPTIONS = [ { value: 'S', label: 'S级' }, { value: 'A', label: 'A级' }, { value: 'B', label: 'B级' }, { value: 'C', label: 'C级' }, ]; const CompactSearchBox = ({ onSearch, onSearchFocus, filters = {}, mode, pageSize, trackingFunctions = {}, isMobile = false }) => { // 状态 const [stockOptions, setStockOptions] = useState([]); const [allStocks, setAllStocks] = useState([]); const [industryValue, setIndustryValue] = useState([]); const [sort, setSort] = useState('new'); const [importance, setImportance] = useState([]); const [tradingTimeRange, setTradingTimeRange] = useState(null); const [inputValue, setInputValue] = useState(''); // Redux const dispatch = useDispatch(); const industryData = useSelector(selectIndustryData); const industryLoading = useSelector(selectIndustryLoading); const reduxAllStocks = useSelector((state) => state.stock.allStocks); // 防抖搜索 const debouncedSearchRef = useRef(null); // 存储股票选择时的显示值(代码+名称),用于 useEffect 同步时显示完整信息 const stockDisplayValueRef = useRef(null); const triggerSearch = useCallback((params) => { logger.debug('CompactSearchBox', '触发搜索', { params }); onSearch(params); }, [onSearch]); useEffect(() => { debouncedSearchRef.current = debounce((params) => { triggerSearch(params); }, 300); return () => { if (debouncedSearchRef.current) { debouncedSearchRef.current.cancel(); } }; }, [triggerSearch]); // 加载股票数据(从 Redux 获取) useEffect(() => { if (!reduxAllStocks || reduxAllStocks.length === 0) { dispatch(loadAllStocks()); } }, [dispatch, reduxAllStocks]); // 同步 Redux 数据到本地状态 useEffect(() => { if (reduxAllStocks && reduxAllStocks.length > 0) { setAllStocks(reduxAllStocks); } }, [reduxAllStocks]); // 预加载行业数据(解决第一次点击无数据问题) useEffect(() => { if (!industryData || industryData.length === 0) { dispatch(fetchIndustryData()); } }, [dispatch, industryData]); // 初始化筛选条件 const findIndustryPath = useCallback((targetCode, data, currentPath = []) => { if (!data || data.length === 0) return null; for (const item of data) { const newPath = [...currentPath, item.value]; if (item.value === targetCode) { return newPath; } if (item.children && item.children.length > 0) { const found = findIndustryPath(targetCode, item.children, newPath); if (found) return found; } } return null; }, []); useEffect(() => { if (!filters) return; if (filters.sort) setSort(filters.sort); if (filters.importance) { const importanceArray = filters.importance === 'all' ? [] : filters.importance.split(',').map(v => v.trim()).filter(Boolean); setImportance(importanceArray); } else { setImportance([]); } if (filters.industry_code && industryData && industryData.length > 0 && (!industryValue || industryValue.length === 0)) { const path = findIndustryPath(filters.industry_code, industryData); if (path) { setIndustryValue(path); } } else if (!filters.industry_code && industryValue && industryValue.length > 0) { setIndustryValue([]); } if (filters.q) { // 如果是股票选择触发的搜索,使用存储的显示值(代码+名称) if (stockDisplayValueRef.current && stockDisplayValueRef.current.code === filters.q) { setInputValue(stockDisplayValueRef.current.displayValue); } else { setInputValue(filters.q); // 清除已失效的显示值缓存 stockDisplayValueRef.current = null; } } else if (!filters.q) { setInputValue(''); stockDisplayValueRef.current = null; } const hasTimeInFilters = filters.start_date || filters.end_date || filters.recent_days; if (hasTimeInFilters && (!tradingTimeRange || !tradingTimeRange.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')}`; } const timeRange = { start_date: filters.start_date || '', end_date: filters.end_date || '', recent_days: filters.recent_days || '', label: inferredLabel, key: inferredKey }; setTradingTimeRange(timeRange); } else if (!hasTimeInFilters && tradingTimeRange) { setTradingTimeRange(null); } }, [filters, industryData, findIndustryPath, industryValue, tradingTimeRange]); // 搜索股票 const handleSearch = (value) => { if (!value || !allStocks || allStocks.length === 0) { setStockOptions([]); return; } const results = stockService.fuzzySearch(value, allStocks, 10); const options = results.map(stock => ({ value: stock.code, label: (
{stock.code} {stock.name}
), stockInfo: stock })); setStockOptions(options); }; const buildFilterParams = useCallback((overrides = {}) => { const sortValue = overrides.sort ?? sort; let actualSort = sortValue; let returnType; if (sortValue === 'returns_avg') { actualSort = 'returns'; returnType = 'avg'; } else if (sortValue === 'returns_week') { actualSort = 'returns'; returnType = 'week'; } let importanceValue = overrides.importance ?? importance; if (Array.isArray(importanceValue)) { importanceValue = importanceValue.length === 0 ? 'all' : importanceValue.join(','); } const result = { sort: actualSort, importance: importanceValue, q: (overrides.q ?? filters.q) ?? '', industry_code: overrides.industry_code ?? (industryValue?.join(',') || ''), start_date: overrides.start_date ?? (tradingTimeRange?.start_date || ''), end_date: overrides.end_date ?? (tradingTimeRange?.end_date || ''), recent_days: overrides.recent_days ?? (tradingTimeRange?.recent_days || ''), ...overrides, page: 1, }; delete result.per_page; if (returnType) { result.return_type = returnType; } if (mode !== undefined && mode !== null) { result.mode = mode; } if (pageSize !== undefined && pageSize !== null) { result.per_page = pageSize; } return result; }, [sort, importance, filters.q, industryValue, tradingTimeRange, mode, pageSize]); const handleStockSelect = (_value, option) => { const stockInfo = option.stockInfo; if (stockInfo) { if (trackingFunctions.trackRelatedStockClicked) { trackingFunctions.trackRelatedStockClicked({ stockCode: stockInfo.code, stockName: stockInfo.name, source: 'search_box_autocomplete', timestamp: new Date().toISOString(), }); } const displayValue = `${stockInfo.code} ${stockInfo.name}`; setInputValue(displayValue); // 存储显示值,供 useEffect 同步时使用 stockDisplayValueRef.current = { code: stockInfo.code, displayValue }; const params = buildFilterParams({ q: stockInfo.code, // 接口只传代码 industry_code: '' }); triggerSearch(params); } }; const handleImportanceChange = (value) => { setImportance(value); if (debouncedSearchRef.current) { debouncedSearchRef.current.cancel(); } const importanceStr = value.length === 0 ? 'all' : value.join(','); if (trackingFunctions.trackNewsFilterApplied) { trackingFunctions.trackNewsFilterApplied({ filterType: 'importance', filterValue: importanceStr, timestamp: new Date().toISOString(), }); } const params = buildFilterParams({ importance: importanceStr }); triggerSearch(params); }; const handleSortChange = (value) => { setSort(value); if (debouncedSearchRef.current) { debouncedSearchRef.current.cancel(); } if (trackingFunctions.trackNewsSorted) { trackingFunctions.trackNewsSorted({ sortBy: value, previousSortBy: sort, timestamp: new Date().toISOString(), }); } const params = buildFilterParams({ sort: value }); triggerSearch(params); }; const handleIndustryChange = (value) => { setIndustryValue(value); if (debouncedSearchRef.current) { debouncedSearchRef.current.cancel(); } if (trackingFunctions.trackNewsFilterApplied) { trackingFunctions.trackNewsFilterApplied({ filterType: 'industry', filterValue: value?.[value.length - 1] || '', timestamp: new Date().toISOString(), }); } const params = buildFilterParams({ industry_code: value?.[value.length - 1] || '' }); triggerSearch(params); }; const handleTradingTimeChange = (timeConfig) => { if (!timeConfig) { setTradingTimeRange(null); if (trackingFunctions.trackNewsFilterApplied) { trackingFunctions.trackNewsFilterApplied({ filterType: 'time_range', filterValue: 'cleared', timestamp: new Date().toISOString(), }); } const params = buildFilterParams({ start_date: '', end_date: '', recent_days: '' }); triggerSearch(params); return; } const { range, type, label, key } = timeConfig; let params = {}; if (type === 'recent_days') { params.recent_days = range; params.start_date = ''; params.end_date = ''; } else { params.start_date = range[0].format('YYYY-MM-DD HH:mm:ss'); params.end_date = range[1].format('YYYY-MM-DD HH:mm:ss'); params.recent_days = ''; } setTradingTimeRange({ ...params, label, key }); if (trackingFunctions.trackNewsFilterApplied) { trackingFunctions.trackNewsFilterApplied({ filterType: 'time_range', filterValue: label, timeRangeType: type, timestamp: new Date().toISOString(), }); } const searchParams = buildFilterParams({ ...params, mode }); triggerSearch(searchParams); }; const handleMainSearch = () => { if (debouncedSearchRef.current) { debouncedSearchRef.current.cancel(); } const params = buildFilterParams({ q: inputValue, industry_code: '' }); if (trackingFunctions.trackNewsSearched && inputValue) { trackingFunctions.trackNewsSearched({ searchQuery: inputValue, searchType: 'main_search', filters: params, timestamp: new Date().toISOString(), }); } triggerSearch(params); }; const handleInputChange = (value) => { setInputValue(value); }; const handleReset = () => { setInputValue(''); setStockOptions([]); setIndustryValue([]); setSort('new'); setImportance([]); setTradingTimeRange(null); if (trackingFunctions.trackNewsFilterApplied) { trackingFunctions.trackNewsFilterApplied({ filterType: 'reset', filterValue: 'all_filters_cleared', timestamp: new Date().toISOString(), }); } const resetParams = { q: '', industry_code: '', sort: 'new', importance: 'all', start_date: '', end_date: '', recent_days: '', page: 1, _forceRefresh: Date.now() }; onSearch(resetParams); }; const handleCascaderFocus = async () => { if (!industryData || industryData.length === 0) { dispatch(fetchIndustryData()); } }; return (
{/* 第一行:搜索框 + 日期筛选 */} {/* 搜索框 - flex: 1 占满剩余空间 */} { if (e.key === 'Enter') { handleMainSearch(); } }} style={{ flex: 1, minWidth: isMobile ? 100 : 200 }} className="gold-placeholder" > } placeholder="搜索股票/话题..." style={{ border: 'none', background: 'transparent', color: PROFESSIONAL_COLORS.text.primary, boxShadow: 'none' }} /> {/* 分隔线 - H5 时隐藏 */} {!isMobile && } {/* 日期筛选按钮组 */}
{/* 第二行:筛选条件 */} {/* 左侧筛选 */} {/* 行业筛选 */} {isMobile ? '行业' : '行业筛选'} } changeOnSelect showSearch={{ filter: (inputValue, path) => path.some(option => option.label.toLowerCase().includes(inputValue.toLowerCase()) ) }} allowClear expandTrigger="hover" displayRender={(labels) => labels[labels.length - 1] || (isMobile ? '行业' : '行业筛选')} disabled={industryLoading} style={{ minWidth: isMobile ? 70 : 80 }} suffixIcon={null} className="transparent-cascader" /> {/* 事件等级 */} {isMobile ? '等级' : '事件等级'} } maxTagCount={0} maxTagPlaceholder={(omittedValues) => isMobile ? `${omittedValues.length}项` : `已选 ${omittedValues.length} 项`} className="bracket-select" > {IMPORTANCE_OPTIONS.map(opt => ( ))} {/* 右侧排序和重置 */} {/* 排序 */} {SORT_OPTIONS.map(opt => ( ))} {/* 重置按钮 */}
); }; export default CompactSearchBox;