// 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 } from 'antd'; import { SearchOutlined, CloseCircleOutlined, StockOutlined, FilterOutlined, CalendarOutlined, SortAscendingOutlined } 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 { stockService } from '../../../services/stockService'; import { logger } from '../../../utils/logger'; import TradingTimeFilter from './TradingTimeFilter'; import { PROFESSIONAL_COLORS } from '../../../constants/professionalTheme'; const { Option } = AntSelect; const CompactSearchBox = ({ onSearch, onSearchFocus, filters = {}, mode, pageSize, trackingFunctions = {} }) => { // 状态 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 debouncedSearchRef = 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]); // 加载股票数据 useEffect(() => { const loadStocks = async () => { const response = await stockService.getAllStocks(); if (response.success && response.data) { setAllStocks(response.data); } }; loadStocks(); }, []); // 初始化筛选条件 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) { setInputValue(filters.q); } else if (!filters.q) { setInputValue(''); } 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?.[industryValue.length - 1] || ''), 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(), }); } setInputValue(`${stockInfo.code} ${stockInfo.name}`); 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 (
{/* 单行紧凑布局 - 移动端自动换行 */} {/* 搜索框 */} { if (e.key === 'Enter') { handleMainSearch(); } }} style={{ width: window.innerWidth < 768 ? '100%' : 240, minWidth: window.innerWidth < 768 ? 0 : 240 }} > } style={{ borderRadius: '8px', border: `1px solid ${PROFESSIONAL_COLORS.border.default}`, boxShadow: `0 2px 8px rgba(255, 195, 0, 0.1)`, background: PROFESSIONAL_COLORS.background.secondary, color: PROFESSIONAL_COLORS.text.primary }} /> {/* 时间筛选 */}
{/* 行业筛选 */} path.some(option => option.label.toLowerCase().includes(inputValue.toLowerCase()) ) }} allowClear expandTrigger="hover" displayRender={(labels) => labels[labels.length - 1] || '行业'} disabled={industryLoading} style={{ width: window.innerWidth < 768 ? '100%' : 120, minWidth: window.innerWidth < 768 ? 0 : 120, borderRadius: '8px' }} suffixIcon={} /> {/* 重要性筛选 */} `已选 ${omittedValues.length} 项`} > {/* 排序 */} } > {/* 重置按钮 */}
{/* 激活的筛选标签(如果有的话) */} {(inputValue || industryValue.length > 0 || importance.length > 0 || tradingTimeRange || sort !== 'new') && (
{inputValue && ( { setInputValue(''); const params = buildFilterParams({ q: '' }); triggerSearch(params); }} color="blue"> 搜索: {inputValue} )} {tradingTimeRange && ( { setTradingTimeRange(null); const params = buildFilterParams({ start_date: '', end_date: '', recent_days: '' }); triggerSearch(params); }} color="green"> {tradingTimeRange.label} )} {industryValue.length > 0 && industryData && ( { setIndustryValue([]); const params = buildFilterParams({ industry_code: '' }); triggerSearch(params); }} color="orange"> 行业: {(() => { const findLabel = (code, data) => { for (const item of data) { if (code.startsWith(item.value)) { if (item.value === code) { return item.label; } else { return findLabel(code, item.children); } } } return null; }; const lastLevelCode = industryValue[industryValue.length - 1]; return findLabel(lastLevelCode, industryData); })()} )} {importance.length > 0 && ( { setImportance([]); const params = buildFilterParams({ importance: 'all' }); triggerSearch(params); }} color="purple"> 重要性: {importance.map(imp => ({ 'S': '极高', 'A': '高', 'B': '中', 'C': '低' }[imp] || imp)).join(', ')} )} {sort && sort !== 'new' && ( { setSort('new'); const params = buildFilterParams({ sort: 'new' }); triggerSearch(params); }} color="cyan"> 排序: {({ 'hot': '最热', 'importance': '重要性', 'returns_avg': '平均收益', 'returns_week': '周收益' }[sort] || sort)} )}
)}
); }; export default CompactSearchBox;