813 lines
32 KiB
JavaScript
813 lines
32 KiB
JavaScript
// src/views/Community/components/UnifiedSearchBox.js
|
||
// 搜索组件:三行布局(主搜索 + 热门概念 + 筛选区)
|
||
import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
||
import {
|
||
Card, Input, Cascader, Button, Space, Tag, AutoComplete, Select as AntSelect
|
||
} from 'antd';
|
||
import {
|
||
SearchOutlined, CloseCircleOutlined, StockOutlined
|
||
} 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 PopularKeywords from './PopularKeywords';
|
||
import TradingTimeFilter from './TradingTimeFilter';
|
||
|
||
const { Option } = AntSelect;
|
||
|
||
const UnifiedSearchBox = ({
|
||
onSearch,
|
||
onSearchFocus,
|
||
popularKeywords = [],
|
||
filters = {},
|
||
mode, // 显示模式(如:vertical, horizontal 等)
|
||
pageSize // 每页显示数量
|
||
}) => {
|
||
|
||
// 其他状态
|
||
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 loadIndustryData = useCallback(() => {
|
||
if (!industryData) {
|
||
dispatch(fetchIndustryData());
|
||
}
|
||
}, [dispatch, industryData]);
|
||
|
||
// 搜索触发函数
|
||
const triggerSearch = useCallback((params) => {
|
||
logger.debug('UnifiedSearchBox', '【5/5】✅ 最终触发搜索 - 调用onSearch回调', {
|
||
params: params,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
onSearch(params);
|
||
}, [onSearch]);
|
||
|
||
// ✅ 创建防抖的搜索函数(300ms 延迟)
|
||
const debouncedSearchRef = useRef(null);
|
||
|
||
useEffect(() => {
|
||
// 创建防抖函数,使用 triggerSearch 而不是直接调用 onSearch
|
||
debouncedSearchRef.current = debounce((params) => {
|
||
logger.debug('UnifiedSearchBox', '⏱️ 防抖延迟结束,执行搜索', {
|
||
params: params,
|
||
delayMs: 300
|
||
});
|
||
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);
|
||
logger.debug('UnifiedSearchBox', '股票数据加载成功', {
|
||
count: response.data.length
|
||
});
|
||
}
|
||
};
|
||
|
||
loadStocks();
|
||
}, []);
|
||
|
||
// Cascader 获得焦点时加载数据
|
||
const handleCascaderFocus = async () => {
|
||
if (!industryData || industryData.length === 0) {
|
||
logger.debug('UnifiedSearchBox', 'Cascader 获得焦点,开始加载行业数据');
|
||
await loadIndustryData();
|
||
}
|
||
};
|
||
|
||
// 从 props.filters 初始化所有内部状态 (只在组件首次挂载时执行)
|
||
// 辅助函数:递归查找行业代码的完整路径
|
||
const findIndustryPath = React.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;
|
||
}, []);
|
||
|
||
// ✅ 从 props.filters 初始化筛选条件和输入框值
|
||
useEffect(() => {
|
||
if (!filters) return;
|
||
|
||
// 初始化排序
|
||
if (filters.sort) setSort(filters.sort);
|
||
|
||
// 初始化重要性(字符串解析为数组)
|
||
if (filters.importance) {
|
||
const importanceArray = filters.importance === 'all'
|
||
? [] // 'all' 对应空数组(不显示任何选中)
|
||
: filters.importance.split(',').map(v => v.trim()).filter(Boolean);
|
||
setImportance(importanceArray);
|
||
logger.debug('UnifiedSearchBox', '初始化重要性', {
|
||
filters_importance: filters.importance,
|
||
importanceArray
|
||
});
|
||
} else {
|
||
setImportance([]);
|
||
}
|
||
|
||
// ✅ 初始化行业分类(需要 industryData 加载完成)
|
||
// ⚠️ 只在 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);
|
||
logger.debug('UnifiedSearchBox', '初始化行业分类', {
|
||
industry_code: filters.industry_code,
|
||
path
|
||
});
|
||
}
|
||
} else if (!filters.industry_code && industryValue && industryValue.length > 0) {
|
||
// 如果 filters 中没有行业代码,但本地有值,清空本地值
|
||
setIndustryValue([]);
|
||
logger.debug('UnifiedSearchBox', '清空行业分类(filters中无值)');
|
||
}
|
||
|
||
// ✅ 同步 filters.q 到输入框显示值
|
||
if (filters.q) {
|
||
setInputValue(filters.q);
|
||
} else if (!filters.q) {
|
||
// 如果 filters 中没有搜索关键词,清空输入框
|
||
setInputValue('');
|
||
}
|
||
|
||
// ✅ 初始化时间筛选(从 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) => {
|
||
if (!value || !allStocks || allStocks.length === 0) {
|
||
setStockOptions([]);
|
||
return;
|
||
}
|
||
|
||
// 使用 stockService 进行模糊搜索
|
||
const results = stockService.fuzzySearch(value, allStocks, 10);
|
||
|
||
// 转换为 AutoComplete 选项格式
|
||
const options = results.map(stock => ({
|
||
value: stock.code,
|
||
label: (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<StockOutlined style={{ color: '#1890ff' }} />
|
||
<span style={{ fontWeight: 500, color: '#333' }}>{stock.code}</span>
|
||
<span style={{ color: '#666' }}>{stock.name}</span>
|
||
</div>
|
||
),
|
||
// 保存完整的股票信息,用于选中后显示
|
||
stockInfo: stock
|
||
}));
|
||
|
||
setStockOptions(options);
|
||
logger.debug('UnifiedSearchBox', '股票模糊搜索', {
|
||
query: value,
|
||
resultCount: options.length
|
||
});
|
||
};
|
||
|
||
// ✅ 选中股票(从下拉选择) - 更新输入框并触发搜索
|
||
const handleStockSelect = (_value, option) => {
|
||
const stockInfo = option.stockInfo;
|
||
if (stockInfo) {
|
||
logger.debug('UnifiedSearchBox', '选中股票', {
|
||
code: stockInfo.code,
|
||
name: stockInfo.name
|
||
});
|
||
|
||
// 更新输入框显示
|
||
setInputValue(`${stockInfo.code} ${stockInfo.name}`);
|
||
|
||
// 直接构建参数并触发搜索 - 使用股票代码作为 q 参数
|
||
const params = buildFilterParams({
|
||
q: stockInfo.code, // 使用股票代码作为搜索关键词
|
||
industry_code: ''
|
||
});
|
||
logger.debug('UnifiedSearchBox', '自动触发股票搜索', params);
|
||
triggerSearch(params);
|
||
}
|
||
};
|
||
|
||
// ✅ 重要性变化(立即执行)- 支持多选
|
||
const handleImportanceChange = (value) => {
|
||
logger.debug('UnifiedSearchBox', '重要性值改变', {
|
||
oldValue: importance,
|
||
newValue: value
|
||
});
|
||
|
||
setImportance(value);
|
||
|
||
// 取消之前的防抖搜索
|
||
if (debouncedSearchRef.current) {
|
||
debouncedSearchRef.current.cancel();
|
||
}
|
||
|
||
// 转换为逗号分隔字符串传给后端(空数组表示"全部")
|
||
const importanceStr = value.length === 0 ? 'all' : value.join(',');
|
||
|
||
// 立即触发搜索
|
||
const params = buildFilterParams({ importance: importanceStr });
|
||
logger.debug('UnifiedSearchBox', '重要性改变,立即触发搜索', params);
|
||
|
||
triggerSearch(params);
|
||
};
|
||
|
||
// ✅ 排序变化(立即触发搜索)
|
||
const handleSortChange = (value) => {
|
||
logger.debug('UnifiedSearchBox', '排序值改变', {
|
||
oldValue: sort,
|
||
newValue: value
|
||
});
|
||
setSort(value);
|
||
|
||
// 取消之前的防抖搜索
|
||
if (debouncedSearchRef.current) {
|
||
debouncedSearchRef.current.cancel();
|
||
}
|
||
|
||
// 立即触发搜索
|
||
const params = buildFilterParams({ sort: value });
|
||
logger.debug('UnifiedSearchBox', '排序改变,立即触发搜索', params);
|
||
triggerSearch(params);
|
||
};
|
||
|
||
// ✅ 行业分类变化(立即触发搜索)
|
||
const handleIndustryChange = (value) => {
|
||
logger.debug('UnifiedSearchBox', '行业分类值改变', {
|
||
oldValue: industryValue,
|
||
newValue: value
|
||
});
|
||
setIndustryValue(value);
|
||
|
||
// 取消之前的防抖搜索
|
||
if (debouncedSearchRef.current) {
|
||
debouncedSearchRef.current.cancel();
|
||
}
|
||
|
||
// 立即触发搜索
|
||
const params = buildFilterParams({
|
||
industry_code: value?.[value.length - 1] || ''
|
||
});
|
||
logger.debug('UnifiedSearchBox', '行业改变,立即触发搜索', params);
|
||
|
||
triggerSearch(params);
|
||
};
|
||
|
||
// ✅ 热门概念点击处理(立即搜索,不使用防抖) - 更新输入框并触发搜索
|
||
const handleKeywordClick = (keyword) => {
|
||
// 更新输入框显示
|
||
setInputValue(keyword);
|
||
|
||
// 立即触发搜索(取消之前的防抖)
|
||
if (debouncedSearchRef.current) {
|
||
debouncedSearchRef.current.cancel();
|
||
}
|
||
|
||
const params = buildFilterParams({
|
||
q: keyword,
|
||
industry_code: ''
|
||
});
|
||
logger.debug('UnifiedSearchBox', '热门概念点击,立即触发搜索', {
|
||
keyword,
|
||
params
|
||
});
|
||
triggerSearch(params);
|
||
};
|
||
|
||
// ✅ 交易时段筛选变化(立即触发搜索)
|
||
const handleTradingTimeChange = (timeConfig) => {
|
||
if (!timeConfig) {
|
||
// 清空筛选
|
||
setTradingTimeRange(null);
|
||
const params = buildFilterParams({
|
||
start_date: '',
|
||
end_date: '',
|
||
recent_days: ''
|
||
});
|
||
triggerSearch(params);
|
||
return;
|
||
}
|
||
|
||
const { range, type, label, key } = timeConfig;
|
||
let params = {};
|
||
|
||
if (type === 'recent_days') {
|
||
// 近一周/近一月使用 recent_days
|
||
params.recent_days = range;
|
||
params.start_date = '';
|
||
params.end_date = '';
|
||
} else {
|
||
// 其他使用 start_date + end_date
|
||
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 });
|
||
|
||
// 立即触发搜索
|
||
const searchParams = buildFilterParams({ ...params, mode });
|
||
logger.debug('UnifiedSearchBox', '交易时段筛选变化,立即触发搜索', {
|
||
timeConfig,
|
||
params: searchParams
|
||
});
|
||
triggerSearch(searchParams);
|
||
};
|
||
|
||
// 主搜索(点击搜索按钮或回车)
|
||
const handleMainSearch = () => {
|
||
// 取消之前的防抖
|
||
if (debouncedSearchRef.current) {
|
||
debouncedSearchRef.current.cancel();
|
||
}
|
||
|
||
// 构建参数并触发搜索 - 使用用户输入作为 q 参数
|
||
const params = buildFilterParams({
|
||
q: inputValue, // 使用用户输入(可能是话题、股票代码、股票名称等)
|
||
industry_code: ''
|
||
});
|
||
|
||
logger.debug('UnifiedSearchBox', '主搜索触发', {
|
||
inputValue,
|
||
params
|
||
});
|
||
triggerSearch(params);
|
||
};
|
||
|
||
// ✅ 处理输入变化 - 更新本地输入状态
|
||
const handleInputChange = (value) => {
|
||
logger.debug('UnifiedSearchBox', '输入变化', { value });
|
||
setInputValue(value);
|
||
};
|
||
|
||
// ✅ 生成完整的筛选参数对象 - 直接从 filters 和本地筛选器状态构建
|
||
const buildFilterParams = useCallback((overrides = {}) => {
|
||
logger.debug('UnifiedSearchBox', '🔧 buildFilterParams - 输入参数', {
|
||
overrides: overrides,
|
||
currentState: {
|
||
sort,
|
||
importance,
|
||
industryValue,
|
||
'filters.q': filters.q,
|
||
mode,
|
||
pageSize
|
||
}
|
||
});
|
||
|
||
// 处理排序参数 - 将 returns_avg/returns_week 转换为 sort=returns + return_type
|
||
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 = {
|
||
// 基础参数(overrides 优先级高于本地状态)
|
||
sort: actualSort,
|
||
importance: importanceValue,
|
||
|
||
|
||
// 搜索参数: 统一使用 q 参数进行搜索(话题/股票/关键词)
|
||
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 具有最高优先级
|
||
...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, mode, pageSize]);
|
||
|
||
// ✅ 重置筛选 - 清空所有筛选器并触发搜索
|
||
const handleReset = () => {
|
||
console.log('%c🔄 [重置] 开始重置筛选条件', 'color: #FF4D4F; font-weight: bold;');
|
||
|
||
// 重置所有筛选器状态
|
||
setInputValue(''); // 清空输入框
|
||
setStockOptions([]);
|
||
setIndustryValue([]);
|
||
setSort('new');
|
||
setImportance([]); // 改为空数组
|
||
setTradingTimeRange(null); // 清空交易时段筛选
|
||
|
||
// 输出重置后的完整参数
|
||
const resetParams = {
|
||
q: '',
|
||
industry_code: '',
|
||
sort: 'new',
|
||
importance: 'all', // 传给后端时转为'all'
|
||
start_date: '',
|
||
end_date: '',
|
||
recent_days: '',
|
||
page: 1,
|
||
_forceRefresh: Date.now() // 添加强制刷新标志,确保每次重置都触发更新
|
||
};
|
||
|
||
console.log('%c🔄 [重置] 重置参数', 'color: #FF4D4F;', resetParams);
|
||
logger.debug('UnifiedSearchBox', '重置筛选', resetParams);
|
||
|
||
console.log('%c🔄 [重置] 调用 onSearch', 'color: #FF4D4F;', typeof onSearch);
|
||
onSearch(resetParams);
|
||
|
||
console.log('%c✅ [重置] 重置完成', 'color: #52C41A; font-weight: bold;');
|
||
};
|
||
|
||
// 生成已选条件标签(包含所有筛选条件) - 从 filters 和本地状态读取
|
||
const filterTags = useMemo(() => {
|
||
const tags = [];
|
||
|
||
// 搜索关键词标签 - 从 filters.q 读取
|
||
if (filters.q) {
|
||
tags.push({ key: 'search', label: `搜索: ${filters.q}` });
|
||
}
|
||
|
||
// 行业标签
|
||
if (industryValue && industryValue.length > 0 && industryData) {
|
||
// 递归查找每个层级的 label
|
||
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;
|
||
};
|
||
|
||
// 只显示最后一级的 label
|
||
const lastLevelCode = industryValue[industryValue.length - 1];
|
||
const lastLevelLabel = findLabel(lastLevelCode, industryData);
|
||
|
||
tags.push({
|
||
key: 'industry',
|
||
label: `行业: ${lastLevelLabel}`
|
||
});
|
||
}
|
||
|
||
// 交易时段筛选标签
|
||
if (tradingTimeRange?.label) {
|
||
tags.push({
|
||
key: 'trading_time',
|
||
label: `时间: ${tradingTimeRange.label}`
|
||
});
|
||
}
|
||
|
||
// 重要性标签(多选合并显示为单个标签)
|
||
if (importance && importance.length > 0) {
|
||
const importanceMap = { 'S': '极高', 'A': '高', 'B': '中', 'C': '低' };
|
||
const importanceLabel = importance.map(imp => importanceMap[imp] || imp).join(', ');
|
||
tags.push({ key: 'importance', label: `重要性: ${importanceLabel}` });
|
||
}
|
||
|
||
// 排序标签(排除默认值 'new')
|
||
if (sort && sort !== 'new') {
|
||
let sortLabel;
|
||
if (sort === 'hot') sortLabel = '最热';
|
||
else if (sort === 'importance') sortLabel = '重要性';
|
||
else if (sort === 'returns_avg') sortLabel = '平均收益率';
|
||
else if (sort === 'returns_week') sortLabel = '周收益率';
|
||
else sortLabel = sort;
|
||
tags.push({ key: 'sort', label: `排序: ${sortLabel}` });
|
||
}
|
||
|
||
return tags;
|
||
}, [filters.q, industryValue, importance, sort, tradingTimeRange]);
|
||
|
||
// ✅ 移除单个标签 - 构建新参数并触发搜索
|
||
const handleRemoveTag = (key) => {
|
||
logger.debug('UnifiedSearchBox', '移除标签', { key });
|
||
|
||
// 取消所有待执行的防抖搜索(避免旧的防抖覆盖删除操作)
|
||
if (debouncedSearchRef.current) {
|
||
debouncedSearchRef.current.cancel();
|
||
}
|
||
|
||
if (key === 'search') {
|
||
// 清除搜索关键词和输入框,立即触发搜索
|
||
setInputValue(''); // 清空输入框
|
||
const params = buildFilterParams({ q: '' });
|
||
logger.debug('UnifiedSearchBox', '移除搜索标签后触发搜索', { key, params });
|
||
triggerSearch(params);
|
||
} else if (key === 'industry') {
|
||
// 清除行业选择
|
||
setIndustryValue([]);
|
||
const params = buildFilterParams({ industry_code: '' });
|
||
triggerSearch(params);
|
||
} else if (key === 'trading_time') {
|
||
// 清除交易时段筛选
|
||
setTradingTimeRange(null);
|
||
const params = buildFilterParams({
|
||
start_date: '',
|
||
end_date: '',
|
||
recent_days: ''
|
||
});
|
||
triggerSearch(params);
|
||
} else if (key === 'importance') {
|
||
// 重置重要性为空数组(传给后端为'all')
|
||
setImportance([]);
|
||
const params = buildFilterParams({ importance: 'all' });
|
||
triggerSearch(params);
|
||
} else if (key === 'sort') {
|
||
// 重置排序为默认值
|
||
setSort('new');
|
||
const params = buildFilterParams({ sort: 'new' });
|
||
triggerSearch(params);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div style={{padding: '8px'}}>
|
||
{/* 第三行:行业 + 重要性 + 排序 */}
|
||
<Space style={{ width: '100%', justifyContent: 'space-between' }} size="middle">
|
||
{/* 左侧:筛选器组 */}
|
||
<Space size="small" wrap>
|
||
<span style={{ fontSize: 12, color: '#666', fontWeight: 'bold' }}>筛选:</span>
|
||
{/* 行业分类 */}
|
||
<Cascader
|
||
value={industryValue}
|
||
onChange={handleIndustryChange}
|
||
onFocus={handleCascaderFocus}
|
||
options={industryData || []}
|
||
placeholder="行业分类"
|
||
changeOnSelect
|
||
showSearch={{
|
||
filter: (inputValue, path) =>
|
||
path.some(option =>
|
||
option.label.toLowerCase().includes(inputValue.toLowerCase())
|
||
)
|
||
}}
|
||
allowClear
|
||
expandTrigger="hover"
|
||
displayRender={(labels) => labels.join(' > ')}
|
||
disabled={industryLoading}
|
||
style={{ width: 160 }}
|
||
size="small"
|
||
/>
|
||
|
||
{/* 重要性 */}
|
||
<Space size="small">
|
||
<span style={{ fontSize: 12, color: '#666' }}>重要性:</span>
|
||
<AntSelect
|
||
mode="multiple"
|
||
value={importance}
|
||
onChange={handleImportanceChange}
|
||
style={{ width: 120 }}
|
||
size="small"
|
||
placeholder="全部"
|
||
maxTagCount={3}
|
||
>
|
||
<Option value="S">极高</Option>
|
||
<Option value="A">高</Option>
|
||
<Option value="B">中</Option>
|
||
<Option value="C">低</Option>
|
||
</AntSelect>
|
||
</Space>
|
||
|
||
{/* 搜索图标(可点击) + 搜索框 */}
|
||
<Space.Compact style={{ flex: 1, minWidth: 250 }}>
|
||
<SearchOutlined
|
||
onClick={handleMainSearch}
|
||
style={{
|
||
fontSize: 14,
|
||
padding: '5px 8px',
|
||
background: '#e6f7ff',
|
||
borderRadius: '6px 0 0 6px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
color: '#1890ff',
|
||
cursor: 'pointer',
|
||
transition: 'all 0.3s'
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
e.currentTarget.style.color = '#096dd9';
|
||
e.currentTarget.style.background = '#bae7ff';
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
e.currentTarget.style.color = '#1890ff';
|
||
e.currentTarget.style.background = '#e6f7ff';
|
||
}}
|
||
/>
|
||
<AutoComplete
|
||
value={inputValue}
|
||
onChange={handleInputChange}
|
||
onSearch={handleSearch}
|
||
onSelect={handleStockSelect}
|
||
onFocus={onSearchFocus}
|
||
options={stockOptions}
|
||
placeholder="请输入股票代码/股票名称/相关话题"
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter') {
|
||
handleMainSearch();
|
||
}
|
||
}}
|
||
style={{ flex: 1 }}
|
||
size="small"
|
||
notFoundContent={inputValue && stockOptions.length === 0 ? "未找到匹配的股票" : null}
|
||
/>
|
||
</Space.Compact>
|
||
|
||
{/* 重置按钮 - 现代化设计 */}
|
||
<Button
|
||
icon={<CloseCircleOutlined />}
|
||
onClick={handleReset}
|
||
size="small"
|
||
style={{
|
||
borderRadius: 6,
|
||
border: '1px solid #d9d9d9',
|
||
backgroundColor: '#fff',
|
||
color: '#666',
|
||
fontWeight: 500,
|
||
padding: '4px 10px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 4,
|
||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)'
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
e.currentTarget.style.borderColor = '#ff4d4f';
|
||
e.currentTarget.style.color = '#ff4d4f';
|
||
e.currentTarget.style.backgroundColor = '#fff1f0';
|
||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(255, 77, 79, 0.15)';
|
||
e.currentTarget.style.transform = 'translateY(-1px)';
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
e.currentTarget.style.borderColor = '#d9d9d9';
|
||
e.currentTarget.style.color = '#666';
|
||
e.currentTarget.style.backgroundColor = '#fff';
|
||
e.currentTarget.style.boxShadow = '0 1px 2px rgba(0, 0, 0, 0.05)';
|
||
e.currentTarget.style.transform = 'translateY(0)';
|
||
}}
|
||
>
|
||
重置
|
||
</Button>
|
||
</Space>
|
||
|
||
{/* 右侧:排序 */}
|
||
<Space size="small">
|
||
<span style={{ fontSize: 12, color: '#666' }}>排序:</span>
|
||
<AntSelect
|
||
value={sort}
|
||
onChange={handleSortChange}
|
||
style={{ width: 100 }}
|
||
size="small"
|
||
>
|
||
<Option value="new">最新</Option>
|
||
<Option value="hot">最热</Option>
|
||
<Option value="importance">重要性</Option>
|
||
<Option value="returns_avg">平均收益率</Option>
|
||
<Option value="returns_week">周收益率</Option>
|
||
</AntSelect>
|
||
</Space>
|
||
</Space>
|
||
|
||
{/* 第一行:筛选 + 时间按钮 + 搜索图标 + 搜索框 */}
|
||
<Space wrap style={{ width: '100%', marginBottom: 4, marginTop: 6 }} size="middle">
|
||
<span style={{ fontSize: 14, color: '#666', fontWeight: 'bold' }}>时间筛选:</span>
|
||
|
||
{/* 交易时段筛选 */}
|
||
<TradingTimeFilter
|
||
value={tradingTimeRange?.key || null}
|
||
onChange={handleTradingTimeChange}
|
||
/>
|
||
</Space>
|
||
|
||
{/* 第二行:热门概念 */}
|
||
<div style={{ marginTop: 2 }}>
|
||
<PopularKeywords
|
||
keywords={popularKeywords}
|
||
onKeywordClick={handleKeywordClick}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default UnifiedSearchBox;
|