diff --git a/src/views/Community/components/SearchFilters/CompactSearchBox.js b/src/views/Community/components/SearchFilters/CompactSearchBox.js index e2defe4c..65590524 100644 --- a/src/views/Community/components/SearchFilters/CompactSearchBox.js +++ b/src/views/Community/components/SearchFilters/CompactSearchBox.js @@ -1,641 +1,543 @@ // src/views/Community/components/SearchFilters/CompactSearchBox.js // 紧凑版搜索和筛选组件 - 优化布局 -import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react'; +import React, { useState, useMemo, useEffect, useCallback, useRef } from "react"; import { - Input, Cascader, Button, Space, Tag, AutoComplete, Select as AntSelect, - Tooltip, Divider, Flex -} from 'antd'; + Input, + Cascader, + Button, + Space, + 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'; + SearchOutlined, + CloseCircleOutlined, + StockOutlined, + FilterOutlined, + CalendarOutlined, + SortAscendingOutlined, + ReloadOutlined, + ThunderboltOutlined, +} from "@ant-design/icons"; +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"; + +// 模块化导入 +import { + SORT_OPTIONS, + IMPORTANCE_OPTIONS, +} from "./CompactSearchBox/constants"; +import { + findIndustryPath, + inferTimeRangeFromFilters, + buildFilterParams, +} from "./CompactSearchBox/utils"; 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 + 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(''); + // 状态 + 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); + // 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); + // Refs + const debouncedSearchRef = useRef(null); + const stockDisplayValueRef = useRef(null); - const triggerSearch = useCallback((params) => { - logger.debug('CompactSearchBox', '触发搜索', { params }); - onSearch(params); - }, [onSearch]); + const triggerSearch = useCallback( + (params) => { + logger.debug("CompactSearchBox", "触发搜索", { params }); + onSearch(params); + }, + [onSearch] + ); - useEffect(() => { - debouncedSearchRef.current = debounce((params) => { - triggerSearch(params); - }, 300); + // 创建构建参数的封装函数 + const createFilterParams = useCallback( + (overrides = {}) => + buildFilterParams({ + overrides, + sort, + importance, + filtersQ: filters.q, + industryValue, + tradingTimeRange, + mode, + pageSize, + }), + [sort, importance, filters.q, industryValue, tradingTimeRange, mode, pageSize] + ); - return () => { - if (debouncedSearchRef.current) { - debouncedSearchRef.current.cancel(); - } - }; - }, [triggerSearch]); + // 防抖搜索初始化 + useEffect(() => { + debouncedSearchRef.current = debounce((params) => { + triggerSearch(params); + }, 300); - // 加载股票数据(从 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; - - // 优先使用 _sortDisplay(原始排序值),否则回退到 sort - // 这样可以正确显示 returns_avg, returns_week 等复合排序选项 - if (filters._sortDisplay || filters.sort) { - setSort(filters._sortDisplay || 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 || filters.time_filter_key; - - if (hasTimeInFilters && (!tradingTimeRange || !tradingTimeRange.key)) { - // 优先使用 time_filter_key(来自 useEventFilters 的默认值) - let inferredKey = filters.time_filter_key || 'custom'; - let inferredLabel = ''; - - if (filters.time_filter_key === 'current-trading-day') { - inferredKey = 'current-trading-day'; - inferredLabel = '当前交易日'; - } else if (filters.time_filter_key === 'all') { - inferredKey = 'all'; - inferredLabel = '全部'; - } else 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); + return () => { + if (debouncedSearchRef.current) { + debouncedSearchRef.current.cancel(); + } }; + }, [triggerSearch]); - const buildFilterParams = useCallback((overrides = {}) => { - const sortValue = overrides.sort ?? sort; - let actualSort = sortValue; - let returnType; + // 加载股票数据 + useEffect(() => { + if (!reduxAllStocks || reduxAllStocks.length === 0) { + dispatch(loadAllStocks()); + } + }, [dispatch, reduxAllStocks]); - if (sortValue === 'returns_avg') { - actualSort = 'returns'; - returnType = 'avg'; - } else if (sortValue === 'returns_week') { - actualSort = 'returns'; - returnType = 'week'; - } + useEffect(() => { + if (reduxAllStocks && reduxAllStocks.length > 0) { + setAllStocks(reduxAllStocks); + } + }, [reduxAllStocks]); - let importanceValue = overrides.importance ?? importance; - if (Array.isArray(importanceValue)) { - importanceValue = importanceValue.length === 0 - ? 'all' - : importanceValue.join(','); - } + // 预加载行业数据 + useEffect(() => { + if (!industryData || industryData.length === 0) { + dispatch(fetchIndustryData()); + } + }, [dispatch, industryData]); - // 先展开 overrides,再用处理后的值覆盖,避免 overrides.sort 覆盖 actualSort - const result = { - ...overrides, - sort: actualSort, - // 保留原始排序值用于 UI 显示(如 returns_avg, returns_week) - _sortDisplay: sortValue, - 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 || ''), - page: 1, - }; + // 同步外部 filters 到本地状态 + useEffect(() => { + if (!filters) return; - // 移除不需要的字段 - delete result.per_page; + // 排序 + if (filters._sortDisplay || filters.sort) { + setSort(filters._sortDisplay || filters.sort); + } - // 添加 return_type 参数(用于收益排序) - if (returnType) { - result.return_type = returnType; - } else { - // 确保非收益排序时不带 return_type - delete result.return_type; - } + // 重要性 + if (filters.importance) { + const importanceArray = + filters.importance === "all" + ? [] + : filters.importance + .split(",") + .map((v) => v.trim()) + .filter(Boolean); + setImportance(importanceArray); + } else { + setImportance([]); + } - if (mode !== undefined && mode !== null) { - result.mode = mode; - } - if (pageSize !== undefined && pageSize !== null) { - result.per_page = pageSize; - } + // 行业 + 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([]); + } - return result; - }, [sort, importance, filters.q, industryValue, tradingTimeRange, mode, pageSize]); + // 搜索关键词 + 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 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 hasTimeInFilters = + filters.start_date || + filters.end_date || + filters.recent_days || + filters.time_filter_key; - const displayValue = `${stockInfo.code} ${stockInfo.name}`; - setInputValue(displayValue); - // 存储显示值,供 useEffect 同步时使用 - stockDisplayValueRef.current = { code: stockInfo.code, displayValue }; + if (hasTimeInFilters && (!tradingTimeRange || !tradingTimeRange.key)) { + const timeRange = inferTimeRangeFromFilters(filters); + if (timeRange) { + setTradingTimeRange(timeRange); + } + } else if (!hasTimeInFilters && tradingTimeRange) { + setTradingTimeRange(null); + } + }, [filters, industryData, industryValue, tradingTimeRange]); - const params = buildFilterParams({ - q: stockInfo.code, // 接口只传代码 - industry_code: '' - }); - triggerSearch(params); - } - }; + // 股票搜索 + const handleSearch = (value) => { + if (!value || !allStocks || allStocks.length === 0) { + setStockOptions([]); + return; + } - 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 === 'all') { - // "全部"按钮:清除所有时间限制 - params.start_date = ''; - params.end_date = ''; - params.recent_days = ''; - } else 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" - allowClear={{ - clearIcon: - }} - > - } - placeholder="搜索股票/话题..." - style={{ - border: 'none', - background: 'transparent', - color: PROFESSIONAL_COLORS.text.primary, - boxShadow: 'none' - }} - /> - - - {/* 分隔线 - H5 时隐藏 */} - {!isMobile && } - - {/* 日期筛选按钮组 */} -
- - -
-
- - {/* 第二行:筛选条件 - 主线模式下隐藏(主线模式有自己的筛选器) */} - {mode !== 'mainline' && ( - - {/* 左侧筛选 */} - - {/* 行业筛选 */} - - - {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 => ( - - ))} - - - {/* 重置按钮 */} - - - - )} + 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 handleStockSelect = (_value, option) => { + const stockInfo = option.stockInfo; + if (stockInfo) { + trackingFunctions.trackRelatedStockClicked?.({ + stockCode: stockInfo.code, + stockName: stockInfo.name, + source: "search_box_autocomplete", + timestamp: new Date().toISOString(), + }); + + const displayValue = `${stockInfo.code} ${stockInfo.name}`; + setInputValue(displayValue); + stockDisplayValueRef.current = { code: stockInfo.code, displayValue }; + + const params = createFilterParams({ q: stockInfo.code, industry_code: "" }); + triggerSearch(params); + } + }; + + // 重要性变更 + const handleImportanceChange = (value) => { + setImportance(value); + debouncedSearchRef.current?.cancel(); + + const importanceStr = value.length === 0 ? "all" : value.join(","); + trackingFunctions.trackNewsFilterApplied?.({ + filterType: "importance", + filterValue: importanceStr, + timestamp: new Date().toISOString(), + }); + + triggerSearch(createFilterParams({ importance: importanceStr })); + }; + + // 排序变更 + const handleSortChange = (value) => { + setSort(value); + debouncedSearchRef.current?.cancel(); + + trackingFunctions.trackNewsSorted?.({ + sortBy: value, + previousSortBy: sort, + timestamp: new Date().toISOString(), + }); + + triggerSearch(createFilterParams({ sort: value })); + }; + + // 行业变更 + const handleIndustryChange = (value) => { + setIndustryValue(value); + debouncedSearchRef.current?.cancel(); + + trackingFunctions.trackNewsFilterApplied?.({ + filterType: "industry", + filterValue: value?.[value.length - 1] || "", + timestamp: new Date().toISOString(), + }); + + triggerSearch(createFilterParams({ industry_code: value?.[value.length - 1] || "" })); + }; + + // 时间筛选变更 + const handleTradingTimeChange = (timeConfig) => { + if (!timeConfig) { + setTradingTimeRange(null); + trackingFunctions.trackNewsFilterApplied?.({ + filterType: "time_range", + filterValue: "cleared", + timestamp: new Date().toISOString(), + }); + triggerSearch(createFilterParams({ start_date: "", end_date: "", recent_days: "" })); + return; + } + + const { range, type, label, key } = timeConfig; + let params = {}; + + if (type === "all") { + params = { start_date: "", end_date: "", recent_days: "" }; + } else if (type === "recent_days") { + params = { recent_days: range, start_date: "", end_date: "" }; + } else { + params = { + start_date: range[0].format("YYYY-MM-DD HH:mm:ss"), + end_date: range[1].format("YYYY-MM-DD HH:mm:ss"), + recent_days: "", + }; + } + + setTradingTimeRange({ ...params, label, key }); + trackingFunctions.trackNewsFilterApplied?.({ + filterType: "time_range", + filterValue: label, + timeRangeType: type, + timestamp: new Date().toISOString(), + }); + + triggerSearch(createFilterParams({ ...params, mode })); + }; + + // 主搜索 + const handleMainSearch = () => { + debouncedSearchRef.current?.cancel(); + const params = createFilterParams({ q: inputValue, industry_code: "" }); + + if (inputValue) { + trackingFunctions.trackNewsSearched?.({ + searchQuery: inputValue, + searchType: "main_search", + filters: params, + timestamp: new Date().toISOString(), + }); + } + + triggerSearch(params); + }; + + // 重置 + const handleReset = () => { + setInputValue(""); + setStockOptions([]); + setIndustryValue([]); + setSort("new"); + setImportance([]); + setTradingTimeRange(null); + + trackingFunctions.trackNewsFilterApplied?.({ + filterType: "reset", + filterValue: "all_filters_cleared", + timestamp: new Date().toISOString(), + }); + + onSearch({ + q: "", + industry_code: "", + sort: "new", + importance: "all", + start_date: "", + end_date: "", + recent_days: "", + page: 1, + _forceRefresh: Date.now(), + }); + }; + + const handleCascaderFocus = async () => { + if (!industryData || industryData.length === 0) { + dispatch(fetchIndustryData()); + } + }; + + return ( +
+ {/* 第一行:搜索框 + 日期筛选 */} + + e.key === "Enter" && handleMainSearch()} + style={{ flex: 1, minWidth: isMobile ? 100 : 200 }} + className="gold-placeholder" + allowClear={{ + clearIcon: ( + + ), + }} + > + } + placeholder="搜索股票/话题..." + style={{ + border: "none", + background: "transparent", + color: PROFESSIONAL_COLORS.text.primary, + boxShadow: "none", + }} + /> + + + {!isMobile && ( + + )} + +
+ + +
+
+ + {/* 第二行:筛选条件 */} + {mode !== "mainline" && ( + + + + + {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) => ( + + ))} + + + + + + )} +
+ ); }; export default CompactSearchBox; diff --git a/src/views/Community/components/SearchFilters/CompactSearchBox/constants.js b/src/views/Community/components/SearchFilters/CompactSearchBox/constants.js new file mode 100644 index 00000000..80247ddb --- /dev/null +++ b/src/views/Community/components/SearchFilters/CompactSearchBox/constants.js @@ -0,0 +1,18 @@ +// CompactSearchBox 常量定义 + +// 排序选项常量 +export 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: "周收" }, +]; + +// 重要性等级常量 +export const IMPORTANCE_OPTIONS = [ + { value: "S", label: "S级" }, + { value: "A", label: "A级" }, + { value: "B", label: "B级" }, + { value: "C", label: "C级" }, +]; diff --git a/src/views/Community/components/SearchFilters/CompactSearchBox/index.js b/src/views/Community/components/SearchFilters/CompactSearchBox/index.js new file mode 100644 index 00000000..4eed6d6b --- /dev/null +++ b/src/views/Community/components/SearchFilters/CompactSearchBox/index.js @@ -0,0 +1,4 @@ +// CompactSearchBox 模块导出 + +export { SORT_OPTIONS, IMPORTANCE_OPTIONS } from "./constants"; +export { findIndustryPath, inferTimeRangeFromFilters, buildFilterParams } from "./utils"; diff --git a/src/views/Community/components/SearchFilters/CompactSearchBox/utils.js b/src/views/Community/components/SearchFilters/CompactSearchBox/utils.js new file mode 100644 index 00000000..29c30dd4 --- /dev/null +++ b/src/views/Community/components/SearchFilters/CompactSearchBox/utils.js @@ -0,0 +1,140 @@ +// CompactSearchBox 工具函数 + +import dayjs from "dayjs"; + +/** + * 在行业树中查找指定代码的完整路径 + * @param {string} targetCode - 目标行业代码 + * @param {Array} data - 行业数据树 + * @param {Array} currentPath - 当前路径(递归用) + * @returns {Array|null} - 找到的完整路径,未找到返回 null + */ +export const findIndustryPath = (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; +}; + +/** + * 从 filters 中推断时间范围配置 + * @param {Object} filters - 筛选条件 + * @returns {Object|null} - 时间范围配置 + */ +export const inferTimeRangeFromFilters = (filters) => { + if (!filters) return null; + + const hasTimeInFilters = + filters.start_date || + filters.end_date || + filters.recent_days || + filters.time_filter_key; + + if (!hasTimeInFilters) return null; + + let inferredKey = filters.time_filter_key || "custom"; + let inferredLabel = ""; + + if (filters.time_filter_key === "current-trading-day") { + inferredKey = "current-trading-day"; + inferredLabel = "当前交易日"; + } else if (filters.time_filter_key === "all") { + inferredKey = "all"; + inferredLabel = "全部"; + } else 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")}`; + } + + return { + start_date: filters.start_date || "", + end_date: filters.end_date || "", + recent_days: filters.recent_days || "", + label: inferredLabel, + key: inferredKey, + }; +}; + +/** + * 构建筛选参数 + * @param {Object} options - 配置选项 + * @returns {Object} - 构建的参数对象 + */ +export const buildFilterParams = ({ + overrides = {}, + sort, + importance, + filtersQ, + industryValue, + tradingTimeRange, + mode, + pageSize, +}) => { + 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, + _sortDisplay: sortValue, + importance: importanceValue, + q: overrides.q ?? filtersQ ?? "", + 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 || ""), + page: 1, + }; + + delete result.per_page; + + if (returnType) { + result.return_type = returnType; + } else { + delete result.return_type; + } + + if (mode !== undefined && mode !== null) { + result.mode = mode; + } + if (pageSize !== undefined && pageSize !== null) { + result.per_page = pageSize; + } + + return result; +};