// 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} /> {/* 重置按钮 - 现代化设计 */} {/* 右侧:排序 */} 排序: {/* 第一行:筛选 + 时间按钮 + 搜索图标 + 搜索框 */} 时间筛选: {/* 交易时段筛选 */} {/* 第二行:热门概念 */}
); }; export default UnifiedSearchBox;