// 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: (
{stock.code}
{stock.name}
),
// 保存完整的股票信息,用于选中后显示
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 (
{/* 第三行:行业 + 重要性 + 排序 */}
{/* 左侧:筛选器组 */}
筛选:
{/* 行业分类 */}
path.some(option =>
option.label.toLowerCase().includes(inputValue.toLowerCase())
)
}}
allowClear
expandTrigger="hover"
displayRender={(labels) => labels.join(' > ')}
disabled={industryLoading}
style={{ width: 160 }}
size="small"
/>
{/* 重要性 */}
重要性:
{/* 搜索图标(可点击) + 搜索框 */}
{
e.currentTarget.style.color = '#096dd9';
e.currentTarget.style.background = '#bae7ff';
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = '#1890ff';
e.currentTarget.style.background = '#e6f7ff';
}}
/>
{
if (e.key === 'Enter') {
handleMainSearch();
}
}}
style={{ flex: 1 }}
size="small"
notFoundContent={inputValue && stockOptions.length === 0 ? "未找到匹配的股票" : null}
/>
{/* 重置按钮 - 现代化设计 */}
}
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)';
}}
>
重置
{/* 右侧:排序 */}
排序:
{/* 第一行:筛选 + 时间按钮 + 搜索图标 + 搜索框 */}
时间筛选:
{/* 交易时段筛选 */}
{/* 第二行:热门概念 */}
);
};
export default UnifiedSearchBox;