// 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 => (
))}
{/* 重置按钮 */}
}
onClick={handleReset}
type="text"
style={{ color: PROFESSIONAL_COLORS.text.secondary }}
>
{!isMobile && '重置筛选'}
);
};
export default CompactSearchBox;