// src/views/Community/components/UnifiedSearchBox.js // 搜索组件:三行布局(主搜索 + 热门概念 + 筛选区) import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react'; import { Card, Input, Cascader, Button, Space, Tag, AutoComplete, DatePicker, Select as AntSelect } from 'antd'; import { SearchOutlined, CloseCircleOutlined, StockOutlined } from '@ant-design/icons'; import moment from 'moment'; import dayjs from 'dayjs'; import locale from 'antd/es/date-picker/locale/zh_CN'; 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'; const { RangePicker } = DatePicker; const { Option } = AntSelect; const UnifiedSearchBox = ({ onSearch, onSearchFocus, popularKeywords = [], filters = {} }) => { // 其他状态 const [stockOptions, setStockOptions] = useState([]); // 股票下拉选项列表 const [allStocks, setAllStocks] = useState([]); // 所有股票数据 const [industryValue, setIndustryValue] = useState([]); // 筛选条件状态 const [sort, setSort] = useState('new'); // 排序方式 const [importance, setImportance] = useState('all'); // 重要性 const [dateRange, setDateRange] = 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) setImportance(filters.importance); // ✅ 初始化日期范围 if (filters.date_range) { const parts = filters.date_range.split(' 至 '); if (parts.length === 2) { setDateRange([dayjs(parts[0]), dayjs(parts[1])]); logger.debug('UnifiedSearchBox', '初始化日期范围', { date_range: filters.date_range }); } } // ✅ 初始化行业分类(需要 industryData 加载完成) if (filters.industry_code && industryData && industryData.length > 0) { const path = findIndustryPath(filters.industry_code, industryData); if (path) { setIndustryValue(path); logger.debug('UnifiedSearchBox', '初始化行业分类', { industry_code: filters.industry_code, path }); } } // ✅ 同步 filters.q 到输入框显示值 if (filters.q) { setInputValue(filters.q); } else if (!filters.q) { // 如果 filters 中没有搜索关键词,清空输入框 setInputValue(''); } }, [filters.sort, filters.importance, filters.date_range, filters.industry_code, filters.q, industryData, findIndustryPath]); // 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 handleDateRangeChange = (dates) => { logger.debug('UnifiedSearchBox', '【1/5】日期范围值改变', { oldValue: dateRange, newValue: dates }); setDateRange(dates); // ⚠️ 注意:setState是异步的,此时dateRange仍是旧值 logger.debug('UnifiedSearchBox', '【2/5】调用buildFilterParams前的状态', { dateRange: dateRange, // 旧值 sort: sort, importance: importance, industryValue: industryValue }); // 使用防抖搜索(需要从新值推导参数) const params = { ...buildFilterParams(), date_range: dates ? `${dates[0].format('YYYY-MM-DD')} 至 ${dates[1].format('YYYY-MM-DD')}` : '' }; logger.debug('UnifiedSearchBox', '【3/5】buildFilterParams返回的参数', params); if (debouncedSearchRef.current) { logger.debug('UnifiedSearchBox', '【4/5】调用防抖函数(300ms延迟)'); debouncedSearchRef.current(params); } }; // ✅ 重要性变化(使用防抖) const handleImportanceChange = (value) => { logger.debug('UnifiedSearchBox', '【1/5】重要性值改变', { oldValue: importance, newValue: value }); setImportance(value); // ⚠️ 注意:setState是异步的,此时importance仍是旧值 logger.debug('UnifiedSearchBox', '【2/5】调用buildFilterParams前的状态', { importance: importance, // 旧值 sort: sort, dateRange: dateRange, industryValue: industryValue }); // 使用防抖搜索 const params = buildFilterParams({ importance: value }); logger.debug('UnifiedSearchBox', '【3/5】buildFilterParams返回的参数', params); if (debouncedSearchRef.current) { logger.debug('UnifiedSearchBox', '【4/5】调用防抖函数(300ms延迟)'); debouncedSearchRef.current(params); } }; // ✅ 排序变化(使用防抖) const handleSortChange = (value) => { logger.debug('UnifiedSearchBox', '【1/5】排序值改变', { oldValue: sort, newValue: value }); setSort(value); // ⚠️ 注意:setState是异步的,此时sort仍是旧值 logger.debug('UnifiedSearchBox', '【2/5】调用buildFilterParams前的状态', { sort: sort, // 旧值 importance: importance, dateRange: dateRange, industryValue: industryValue }); // 使用防抖搜索 const params = buildFilterParams({ sort: value }); logger.debug('UnifiedSearchBox', '【3/5】buildFilterParams返回的参数', params); if (debouncedSearchRef.current) { logger.debug('UnifiedSearchBox', '【4/5】调用防抖函数(300ms延迟)'); debouncedSearchRef.current(params); } }; // ✅ 行业分类变化(使用防抖) const handleIndustryChange = (value) => { logger.debug('UnifiedSearchBox', '【1/5】行业分类值改变', { oldValue: industryValue, newValue: value }); setIndustryValue(value); // ⚠️ 注意:setState是异步的,此时industryValue仍是旧值 logger.debug('UnifiedSearchBox', '【2/5】调用buildFilterParams前的状态', { industryValue: industryValue, // 旧值 sort: sort, importance: importance, dateRange: dateRange }); // 使用防抖搜索 (需要从新值推导参数) const params = { ...buildFilterParams(), industry_code: value?.[value.length - 1] || '' }; logger.debug('UnifiedSearchBox', '【3/5】buildFilterParams返回的参数', params); if (debouncedSearchRef.current) { logger.debug('UnifiedSearchBox', '【4/5】调用防抖函数(300ms延迟)'); debouncedSearchRef.current(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 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, dateRange, industryValue, 'filters.q': filters.q } }); const result = { // 基础参数(overrides 优先级高于本地状态) sort: overrides.sort ?? sort, importance: overrides.importance ?? importance, date_range: dateRange ? `${dateRange[0].format('YYYY-MM-DD')} 至 ${dateRange[1].format('YYYY-MM-DD')}` : '', page: 1, // 搜索参数: 统一使用 q 参数进行搜索(话题/股票/关键词) q: (overrides.q ?? filters.q) ?? '', // 行业代码: 取选中路径的最后一级(最具体的行业代码) industry_code: overrides.industry_code ?? (industryValue?.[industryValue.length - 1] || ''), // 最终 overrides 具有最高优先级 ...overrides }; logger.debug('UnifiedSearchBox', '🔧 buildFilterParams - 输出结果', result); return result; }, [sort, importance, dateRange, filters.q, industryValue]); // ✅ 应用筛选(立即搜索,取消防抖) const handleApplyFilters = () => { // 取消之前的防抖搜索 if (debouncedSearchRef.current) { debouncedSearchRef.current.cancel(); } const params = buildFilterParams(); logger.debug('UnifiedSearchBox', '应用筛选,立即触发搜索', params); triggerSearch(params); }; // ✅ 重置筛选 - 清空所有筛选器并触发搜索 const handleReset = () => { // 重置所有筛选器状态 setInputValue(''); // 清空输入框 setStockOptions([]); setIndustryValue([]); setSort('new'); setImportance('all'); setDateRange(null); // 输出重置后的完整参数 const resetParams = { q: '', industry_code: '', sort: 'new', importance: 'all', date_range: '', page: 1 }; logger.debug('UnifiedSearchBox', '重置筛选', resetParams); onSearch(resetParams); }; // 生成已选条件标签(包含所有筛选条件) - 从 filters 和本地状态读取 const filterTags = useMemo(() => { const tags = []; // 搜索关键词标签 - 从 filters.q 读取 if (filters.q) { tags.push({ key: 'search', label: `搜索: ${filters.q}` }); } // 行业标签 if (industryValue && industryValue.length > 0) { const industryLabel = industryValue.slice(1).join(' > '); tags.push({ key: 'industry', label: `行业: ${industryLabel}` }); } // 日期范围标签 if (dateRange && dateRange.length === 2) { const dateLabel = `${dateRange[0].format('YYYY-MM-DD')} 至 ${dateRange[1].format('YYYY-MM-DD')}`; tags.push({ key: 'date_range', label: `日期: ${dateLabel}` }); } // 重要性标签(排除默认值 'all') if (importance && importance !== 'all') { tags.push({ key: 'importance', label: `重要性: ${importance}级` }); } // 排序标签(排除默认值 'new') if (sort && sort !== 'new') { const sortLabel = sort === 'hot' ? '最热' : sort === 'importance' ? '重要性' : sort; tags.push({ key: 'sort', label: `排序: ${sortLabel}` }); } return tags; }, [filters.q, industryValue, dateRange, importance, sort]); // ✅ 移除单个标签 - 构建新参数并触发搜索 const handleRemoveTag = (key) => { logger.debug('UnifiedSearchBox', '移除标签', { key }); 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 === 'date_range') { // 清除日期范围 setDateRange(null); const params = buildFilterParams({ date_range: '' }); triggerSearch(params); } else if (key === 'importance') { // 重置重要性为默认值 setImportance('all'); const params = buildFilterParams({ importance: 'all' }); triggerSearch(params); } else if (key === 'sort') { // 重置排序为默认值 setSort('new'); const params = buildFilterParams({ sort: 'new' }); triggerSearch(params); } }; return ( {/* 第一行:主搜索框 */} { if (e.key === 'Enter') { handleMainSearch(); } }} style={{ flex: 1 }} size="large" notFoundContent={inputValue && stockOptions.length === 0 ? "未找到匹配的股票" : null} /> {/* 第二行:热门概念 */}
{/* 第三行:筛选器 + 排序 */} {/* 左侧:筛选器组 */} 筛选: {/* 行业分类 */} path.some(option => option.label.toLowerCase().includes(inputValue.toLowerCase()) ) }} allowClear expandTrigger="hover" displayRender={(labels) => labels.join(' > ')} disabled={industryLoading} style={{ width: 200 }} size="middle" /> {/* 日期范围 */} {/* 重要性 */} 重要性: {/* 重置按钮 - 现代化设计 */} {/* 右侧:排序 */} 排序: {/* 已选条件标签 */} {filterTags.length > 0 && ( {filterTags.map(tag => ( handleRemoveTag(tag.key)} color="blue" > {tag.label} ))} )}
); }; export default UnifiedSearchBox;