refactor(CompactSearchBox): 提取常量和工具函数到子模块

- constants.js: SORT_OPTIONS, IMPORTANCE_OPTIONS 选项配置
- utils.js: findIndustryPath, inferTimeRangeFromFilters, buildFilterParams
- index.js: 模块统一导出

主文件逻辑更清晰,工具函数可复用

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2026-01-15 11:45:06 +08:00
parent b826b1e7dc
commit 8a7f624589
4 changed files with 672 additions and 608 deletions

View File

@@ -1,45 +1,55 @@
// src/views/Community/components/SearchFilters/CompactSearchBox.js // 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 { import {
Input, Cascader, Button, Space, Tag, AutoComplete, Select as AntSelect, Input,
Tooltip, Divider, Flex Cascader,
} from 'antd'; Button,
Space,
AutoComplete,
Select as AntSelect,
Tooltip,
Divider,
Flex,
} from "antd";
import { import {
SearchOutlined, CloseCircleOutlined, StockOutlined, FilterOutlined, SearchOutlined,
CalendarOutlined, SortAscendingOutlined, ReloadOutlined, ThunderboltOutlined CloseCircleOutlined,
} from '@ant-design/icons'; StockOutlined,
import dayjs from 'dayjs'; FilterOutlined,
import debounce from 'lodash/debounce'; CalendarOutlined,
import { useSelector, useDispatch } from 'react-redux'; SortAscendingOutlined,
import { fetchIndustryData, selectIndustryData, selectIndustryLoading } from '@store/slices/industrySlice'; ReloadOutlined,
import { loadAllStocks } from '@store/slices/stockSlice'; ThunderboltOutlined,
import { stockService } from '@services/stockService'; } from "@ant-design/icons";
import { logger } from '@utils/logger'; import debounce from "lodash/debounce";
import TradingTimeFilter from './TradingTimeFilter'; import { useSelector, useDispatch } from "react-redux";
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme'; import {
import './CompactSearchBox.css'; 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 { 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 = ({ const CompactSearchBox = ({
onSearch, onSearch,
onSearchFocus, onSearchFocus,
@@ -47,16 +57,16 @@ const CompactSearchBox = ({
mode, mode,
pageSize, pageSize,
trackingFunctions = {}, trackingFunctions = {},
isMobile = false isMobile = false,
}) => { }) => {
// 状态 // 状态
const [stockOptions, setStockOptions] = useState([]); const [stockOptions, setStockOptions] = useState([]);
const [allStocks, setAllStocks] = useState([]); const [allStocks, setAllStocks] = useState([]);
const [industryValue, setIndustryValue] = useState([]); const [industryValue, setIndustryValue] = useState([]);
const [sort, setSort] = useState('new'); const [sort, setSort] = useState("new");
const [importance, setImportance] = useState([]); const [importance, setImportance] = useState([]);
const [tradingTimeRange, setTradingTimeRange] = useState(null); const [tradingTimeRange, setTradingTimeRange] = useState(null);
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState("");
// Redux // Redux
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -64,16 +74,35 @@ const CompactSearchBox = ({
const industryLoading = useSelector(selectIndustryLoading); const industryLoading = useSelector(selectIndustryLoading);
const reduxAllStocks = useSelector((state) => state.stock.allStocks); const reduxAllStocks = useSelector((state) => state.stock.allStocks);
// 防抖搜索 // Refs
const debouncedSearchRef = useRef(null); const debouncedSearchRef = useRef(null);
// 存储股票选择时的显示值(代码+名称),用于 useEffect 同步时显示完整信息
const stockDisplayValueRef = useRef(null); const stockDisplayValueRef = useRef(null);
const triggerSearch = useCallback((params) => { const triggerSearch = useCallback(
logger.debug('CompactSearchBox', '触发搜索', { params }); (params) => {
logger.debug("CompactSearchBox", "触发搜索", { params });
onSearch(params); onSearch(params);
}, [onSearch]); },
[onSearch]
);
// 创建构建参数的封装函数
const createFilterParams = useCallback(
(overrides = {}) =>
buildFilterParams({
overrides,
sort,
importance,
filtersQ: filters.q,
industryValue,
tradingTimeRange,
mode,
pageSize,
}),
[sort, importance, filters.q, industryValue, tradingTimeRange, mode, pageSize]
);
// 防抖搜索初始化
useEffect(() => { useEffect(() => {
debouncedSearchRef.current = debounce((params) => { debouncedSearchRef.current = debounce((params) => {
triggerSearch(params); triggerSearch(params);
@@ -86,128 +115,102 @@ const CompactSearchBox = ({
}; };
}, [triggerSearch]); }, [triggerSearch]);
// 加载股票数据(从 Redux 获取) // 加载股票数据
useEffect(() => { useEffect(() => {
if (!reduxAllStocks || reduxAllStocks.length === 0) { if (!reduxAllStocks || reduxAllStocks.length === 0) {
dispatch(loadAllStocks()); dispatch(loadAllStocks());
} }
}, [dispatch, reduxAllStocks]); }, [dispatch, reduxAllStocks]);
// 同步 Redux 数据到本地状态
useEffect(() => { useEffect(() => {
if (reduxAllStocks && reduxAllStocks.length > 0) { if (reduxAllStocks && reduxAllStocks.length > 0) {
setAllStocks(reduxAllStocks); setAllStocks(reduxAllStocks);
} }
}, [reduxAllStocks]); }, [reduxAllStocks]);
// 预加载行业数据(解决第一次点击无数据问题) // 预加载行业数据
useEffect(() => { useEffect(() => {
if (!industryData || industryData.length === 0) { if (!industryData || industryData.length === 0) {
dispatch(fetchIndustryData()); dispatch(fetchIndustryData());
} }
}, [dispatch, industryData]); }, [dispatch, industryData]);
// 初始化筛选条件 // 同步外部 filters 到本地状态
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(() => { useEffect(() => {
if (!filters) return; if (!filters) return;
// 优先使用 _sortDisplay原始排序值否则回退到 sort // 排序
// 这样可以正确显示 returns_avg, returns_week 等复合排序选项
if (filters._sortDisplay || filters.sort) { if (filters._sortDisplay || filters.sort) {
setSort(filters._sortDisplay || filters.sort); setSort(filters._sortDisplay || filters.sort);
} }
// 重要性
if (filters.importance) { if (filters.importance) {
const importanceArray = filters.importance === 'all' const importanceArray =
filters.importance === "all"
? [] ? []
: filters.importance.split(',').map(v => v.trim()).filter(Boolean); : filters.importance
.split(",")
.map((v) => v.trim())
.filter(Boolean);
setImportance(importanceArray); setImportance(importanceArray);
} else { } else {
setImportance([]); setImportance([]);
} }
if (filters.industry_code && industryData && industryData.length > 0 && (!industryValue || industryValue.length === 0)) { // 行业
if (
filters.industry_code &&
industryData &&
industryData.length > 0 &&
(!industryValue || industryValue.length === 0)
) {
const path = findIndustryPath(filters.industry_code, industryData); const path = findIndustryPath(filters.industry_code, industryData);
if (path) { if (path) {
setIndustryValue(path); setIndustryValue(path);
} }
} else if (!filters.industry_code && industryValue && industryValue.length > 0) { } else if (
!filters.industry_code &&
industryValue &&
industryValue.length > 0
) {
setIndustryValue([]); setIndustryValue([]);
} }
// 搜索关键词
if (filters.q) { if (filters.q) {
// 如果是股票选择触发的搜索,使用存储的显示值(代码+名称) if (
if (stockDisplayValueRef.current && stockDisplayValueRef.current.code === filters.q) { stockDisplayValueRef.current &&
stockDisplayValueRef.current.code === filters.q
) {
setInputValue(stockDisplayValueRef.current.displayValue); setInputValue(stockDisplayValueRef.current.displayValue);
} else { } else {
setInputValue(filters.q); setInputValue(filters.q);
// 清除已失效的显示值缓存
stockDisplayValueRef.current = null; stockDisplayValueRef.current = null;
} }
} else if (!filters.q) { } else if (!filters.q) {
setInputValue(''); setInputValue("");
stockDisplayValueRef.current = null; stockDisplayValueRef.current = null;
} }
const hasTimeInFilters = filters.start_date || filters.end_date || filters.recent_days || filters.time_filter_key; // 时间范围
const hasTimeInFilters =
filters.start_date ||
filters.end_date ||
filters.recent_days ||
filters.time_filter_key;
if (hasTimeInFilters && (!tradingTimeRange || !tradingTimeRange.key)) { if (hasTimeInFilters && (!tradingTimeRange || !tradingTimeRange.key)) {
// 优先使用 time_filter_key来自 useEventFilters 的默认值) const timeRange = inferTimeRangeFromFilters(filters);
let inferredKey = filters.time_filter_key || 'custom'; if (timeRange) {
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); setTradingTimeRange(timeRange);
}
} else if (!hasTimeInFilters && tradingTimeRange) { } else if (!hasTimeInFilters && tradingTimeRange) {
setTradingTimeRange(null); setTradingTimeRange(null);
} }
}, [filters, industryData, findIndustryPath, industryValue, tradingTimeRange]); }, [filters, industryData, industryValue, tradingTimeRange]);
// 搜索股票 // 股票搜索
const handleSearch = (value) => { const handleSearch = (value) => {
if (!value || !allStocks || allStocks.length === 0) { if (!value || !allStocks || allStocks.length === 0) {
setStockOptions([]); setStockOptions([]);
@@ -215,231 +218,132 @@ const CompactSearchBox = ({
} }
const results = stockService.fuzzySearch(value, allStocks, 10); const results = stockService.fuzzySearch(value, allStocks, 10);
const options = results.map(stock => ({ const options = results.map((stock) => ({
value: stock.code, value: stock.code,
label: ( label: (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<StockOutlined style={{ color: '#1890ff' }} /> <StockOutlined style={{ color: "#1890ff" }} />
<span style={{ fontWeight: 500, color: '#333' }}>{stock.code}</span> <span style={{ fontWeight: 500, color: "#333" }}>{stock.code}</span>
<span style={{ color: '#666' }}>{stock.name}</span> <span style={{ color: "#666" }}>{stock.name}</span>
</div> </div>
), ),
stockInfo: stock stockInfo: stock,
})); }));
setStockOptions(options); setStockOptions(options);
}; };
const buildFilterParams = useCallback((overrides = {}) => { // 股票选择
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(',');
}
// 先展开 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,
};
// 移除不需要的字段
delete result.per_page;
// 添加 return_type 参数(用于收益排序)
if (returnType) {
result.return_type = returnType;
} else {
// 确保非收益排序时不带 return_type
delete result.return_type;
}
if (mode !== undefined && mode !== null) {
result.mode = mode;
}
if (pageSize !== undefined && pageSize !== null) {
result.per_page = pageSize;
}
return result;
}, [sort, importance, filters.q, industryValue, tradingTimeRange, mode, pageSize]);
const handleStockSelect = (_value, option) => { const handleStockSelect = (_value, option) => {
const stockInfo = option.stockInfo; const stockInfo = option.stockInfo;
if (stockInfo) { if (stockInfo) {
if (trackingFunctions.trackRelatedStockClicked) { trackingFunctions.trackRelatedStockClicked?.({
trackingFunctions.trackRelatedStockClicked({
stockCode: stockInfo.code, stockCode: stockInfo.code,
stockName: stockInfo.name, stockName: stockInfo.name,
source: 'search_box_autocomplete', source: "search_box_autocomplete",
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
}
const displayValue = `${stockInfo.code} ${stockInfo.name}`; const displayValue = `${stockInfo.code} ${stockInfo.name}`;
setInputValue(displayValue); setInputValue(displayValue);
// 存储显示值,供 useEffect 同步时使用
stockDisplayValueRef.current = { code: stockInfo.code, displayValue }; stockDisplayValueRef.current = { code: stockInfo.code, displayValue };
const params = buildFilterParams({ const params = createFilterParams({ q: stockInfo.code, industry_code: "" });
q: stockInfo.code, // 接口只传代码
industry_code: ''
});
triggerSearch(params); triggerSearch(params);
} }
}; };
// 重要性变更
const handleImportanceChange = (value) => { const handleImportanceChange = (value) => {
setImportance(value); setImportance(value);
debouncedSearchRef.current?.cancel();
if (debouncedSearchRef.current) { const importanceStr = value.length === 0 ? "all" : value.join(",");
debouncedSearchRef.current.cancel(); trackingFunctions.trackNewsFilterApplied?.({
} filterType: "importance",
const importanceStr = value.length === 0 ? 'all' : value.join(',');
if (trackingFunctions.trackNewsFilterApplied) {
trackingFunctions.trackNewsFilterApplied({
filterType: 'importance',
filterValue: importanceStr, filterValue: importanceStr,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
}
const params = buildFilterParams({ importance: importanceStr }); triggerSearch(createFilterParams({ importance: importanceStr }));
triggerSearch(params);
}; };
// 排序变更
const handleSortChange = (value) => { const handleSortChange = (value) => {
setSort(value); setSort(value);
debouncedSearchRef.current?.cancel();
if (debouncedSearchRef.current) { trackingFunctions.trackNewsSorted?.({
debouncedSearchRef.current.cancel();
}
if (trackingFunctions.trackNewsSorted) {
trackingFunctions.trackNewsSorted({
sortBy: value, sortBy: value,
previousSortBy: sort, previousSortBy: sort,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
}
const params = buildFilterParams({ sort: value }); triggerSearch(createFilterParams({ sort: value }));
triggerSearch(params);
}; };
// 行业变更
const handleIndustryChange = (value) => { const handleIndustryChange = (value) => {
setIndustryValue(value); setIndustryValue(value);
debouncedSearchRef.current?.cancel();
if (debouncedSearchRef.current) { trackingFunctions.trackNewsFilterApplied?.({
debouncedSearchRef.current.cancel(); filterType: "industry",
} filterValue: value?.[value.length - 1] || "",
if (trackingFunctions.trackNewsFilterApplied) {
trackingFunctions.trackNewsFilterApplied({
filterType: 'industry',
filterValue: value?.[value.length - 1] || '',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
}
const params = buildFilterParams({ triggerSearch(createFilterParams({ industry_code: value?.[value.length - 1] || "" }));
industry_code: value?.[value.length - 1] || ''
});
triggerSearch(params);
}; };
// 时间筛选变更
const handleTradingTimeChange = (timeConfig) => { const handleTradingTimeChange = (timeConfig) => {
if (!timeConfig) { if (!timeConfig) {
setTradingTimeRange(null); setTradingTimeRange(null);
trackingFunctions.trackNewsFilterApplied?.({
if (trackingFunctions.trackNewsFilterApplied) { filterType: "time_range",
trackingFunctions.trackNewsFilterApplied({ filterValue: "cleared",
filterType: 'time_range',
filterValue: 'cleared',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
} triggerSearch(createFilterParams({ start_date: "", end_date: "", recent_days: "" }));
const params = buildFilterParams({
start_date: '',
end_date: '',
recent_days: ''
});
triggerSearch(params);
return; return;
} }
const { range, type, label, key } = timeConfig; const { range, type, label, key } = timeConfig;
let params = {}; let params = {};
if (type === 'all') { if (type === "all") {
// "全部"按钮:清除所有时间限制 params = { start_date: "", end_date: "", recent_days: "" };
params.start_date = ''; } else if (type === "recent_days") {
params.end_date = ''; params = { recent_days: range, start_date: "", end_date: "" };
params.recent_days = '';
} else if (type === 'recent_days') {
params.recent_days = range;
params.start_date = '';
params.end_date = '';
} else { } else {
params.start_date = range[0].format('YYYY-MM-DD HH:mm:ss'); params = {
params.end_date = range[1].format('YYYY-MM-DD HH:mm:ss'); start_date: range[0].format("YYYY-MM-DD HH:mm:ss"),
params.recent_days = ''; end_date: range[1].format("YYYY-MM-DD HH:mm:ss"),
recent_days: "",
};
} }
setTradingTimeRange({ ...params, label, key }); setTradingTimeRange({ ...params, label, key });
trackingFunctions.trackNewsFilterApplied?.({
if (trackingFunctions.trackNewsFilterApplied) { filterType: "time_range",
trackingFunctions.trackNewsFilterApplied({
filterType: 'time_range',
filterValue: label, filterValue: label,
timeRangeType: type, timeRangeType: type,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
}
const searchParams = buildFilterParams({ ...params, mode }); triggerSearch(createFilterParams({ ...params, mode }));
triggerSearch(searchParams);
}; };
// 主搜索
const handleMainSearch = () => { const handleMainSearch = () => {
if (debouncedSearchRef.current) { debouncedSearchRef.current?.cancel();
debouncedSearchRef.current.cancel(); const params = createFilterParams({ q: inputValue, industry_code: "" });
}
const params = buildFilterParams({ if (inputValue) {
q: inputValue, trackingFunctions.trackNewsSearched?.({
industry_code: ''
});
if (trackingFunctions.trackNewsSearched && inputValue) {
trackingFunctions.trackNewsSearched({
searchQuery: inputValue, searchQuery: inputValue,
searchType: 'main_search', searchType: "main_search",
filters: params, filters: params,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
@@ -448,39 +352,32 @@ const CompactSearchBox = ({
triggerSearch(params); triggerSearch(params);
}; };
const handleInputChange = (value) => { // 重置
setInputValue(value);
};
const handleReset = () => { const handleReset = () => {
setInputValue(''); setInputValue("");
setStockOptions([]); setStockOptions([]);
setIndustryValue([]); setIndustryValue([]);
setSort('new'); setSort("new");
setImportance([]); setImportance([]);
setTradingTimeRange(null); setTradingTimeRange(null);
if (trackingFunctions.trackNewsFilterApplied) { trackingFunctions.trackNewsFilterApplied?.({
trackingFunctions.trackNewsFilterApplied({ filterType: "reset",
filterType: 'reset', filterValue: "all_filters_cleared",
filterValue: 'all_filters_cleared',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
}
const resetParams = { onSearch({
q: '', q: "",
industry_code: '', industry_code: "",
sort: 'new', sort: "new",
importance: 'all', importance: "all",
start_date: '', start_date: "",
end_date: '', end_date: "",
recent_days: '', recent_days: "",
page: 1, page: 1,
_forceRefresh: Date.now() _forceRefresh: Date.now(),
}; });
onSearch(resetParams);
}; };
const handleCascaderFocus = async () => { const handleCascaderFocus = async () => {
@@ -488,57 +385,62 @@ const CompactSearchBox = ({
dispatch(fetchIndustryData()); dispatch(fetchIndustryData());
} }
}; };
return ( return (
<div style={{ padding: 0, background: 'transparent' }}> <div style={{ padding: 0, background: "transparent" }}>
{/* 第一行:搜索框 + 日期筛选 */} {/* 第一行:搜索框 + 日期筛选 */}
<Flex <Flex
align="center" align="center"
gap={isMobile ? 8 : 12} gap={isMobile ? 8 : 12}
style={{ style={{
background: 'rgba(255, 255, 255, 0.03)', background: "rgba(255, 255, 255, 0.03)",
border: `1px solid ${PROFESSIONAL_COLORS.gold[500]}`, border: `1px solid ${PROFESSIONAL_COLORS.gold[500]}`,
borderRadius: '24px', borderRadius: "24px",
padding: isMobile ? '2px 4px' : '8px 16px', padding: isMobile ? "2px 4px" : "8px 16px",
marginBottom: isMobile ? 8 : 12 marginBottom: isMobile ? 8 : 12,
}} }}
> >
{/* 搜索框 - flex: 1 占满剩余空间 */}
<AutoComplete <AutoComplete
value={inputValue} value={inputValue}
onChange={handleInputChange} onChange={setInputValue}
onSearch={handleSearch} onSearch={handleSearch}
onSelect={handleStockSelect} onSelect={handleStockSelect}
onFocus={onSearchFocus} onFocus={onSearchFocus}
options={stockOptions} options={stockOptions}
onKeyDown={(e) => { onKeyDown={(e) => e.key === "Enter" && handleMainSearch()}
if (e.key === 'Enter') {
handleMainSearch();
}
}}
style={{ flex: 1, minWidth: isMobile ? 100 : 200 }} style={{ flex: 1, minWidth: isMobile ? 100 : 200 }}
className="gold-placeholder" className="gold-placeholder"
allowClear={{ allowClear={{
clearIcon: <CloseCircleOutlined style={{ color: PROFESSIONAL_COLORS.text.muted, fontSize: 14 }} /> clearIcon: (
<CloseCircleOutlined
style={{ color: PROFESSIONAL_COLORS.text.muted, fontSize: 14 }}
/>
),
}} }}
> >
<Input <Input
prefix={<SearchOutlined style={{ color: PROFESSIONAL_COLORS.gold[500] }} />} prefix={<SearchOutlined style={{ color: PROFESSIONAL_COLORS.gold[500] }} />}
placeholder="搜索股票/话题..." placeholder="搜索股票/话题..."
style={{ style={{
border: 'none', border: "none",
background: 'transparent', background: "transparent",
color: PROFESSIONAL_COLORS.text.primary, color: PROFESSIONAL_COLORS.text.primary,
boxShadow: 'none' boxShadow: "none",
}} }}
/> />
</AutoComplete> </AutoComplete>
{/* 分隔线 - H5 时隐藏 */} {!isMobile && (
{!isMobile && <Divider type="vertical" style={{ height: 24, margin: '0 8px', borderColor: 'rgba(255,255,255,0.15)' }} />} <Divider
type="vertical"
style={{ height: 24, margin: "0 8px", borderColor: "rgba(255,255,255,0.15)" }}
/>
)}
{/* 日期筛选按钮组 */} <div style={{ display: "flex", alignItems: "center", gap: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 0 }}> <CalendarOutlined
<CalendarOutlined style={{ color: PROFESSIONAL_COLORS.gold[500], fontSize: 14, marginRight: 8 }} /> style={{ color: PROFESSIONAL_COLORS.gold[500], fontSize: 14, marginRight: 8 }}
/>
<TradingTimeFilter <TradingTimeFilter
value={tradingTimeRange?.key || null} value={tradingTimeRange?.key || null}
onChange={handleTradingTimeChange} onChange={handleTradingTimeChange}
@@ -548,73 +450,74 @@ const CompactSearchBox = ({
</div> </div>
</Flex> </Flex>
{/* 第二行:筛选条件 - 主线模式下隐藏(主线模式有自己的筛选器) */} {/* 第二行:筛选条件 */}
{mode !== 'mainline' && ( {mode !== "mainline" && (
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
{/* 左侧筛选 */}
<Space size={isMobile ? 4 : 8}> <Space size={isMobile ? 4 : 8}>
{/* 行业筛选 */}
<Cascader <Cascader
value={industryValue} value={industryValue}
onChange={handleIndustryChange} onChange={handleIndustryChange}
onFocus={handleCascaderFocus} onFocus={handleCascaderFocus}
options={industryData || []} options={industryData || []}
placeholder={ placeholder={
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <span style={{ display: "flex", alignItems: "center", gap: 4 }}>
<FilterOutlined style={{ fontSize: 12 }} /> <FilterOutlined style={{ fontSize: 12 }} />
{isMobile ? '行业' : '行业筛选'} {isMobile ? "行业" : "行业筛选"}
</span> </span>
} }
changeOnSelect changeOnSelect
showSearch={{ showSearch={{
filter: (inputValue, path) => filter: (inputValue, path) =>
path.some(option => path.some((option) =>
option.label.toLowerCase().includes(inputValue.toLowerCase()) option.label.toLowerCase().includes(inputValue.toLowerCase())
) ),
}} }}
allowClear allowClear
expandTrigger="hover" expandTrigger="hover"
displayRender={(labels) => labels[labels.length - 1] || (isMobile ? '行业' : '行业筛选')} displayRender={(labels) =>
labels[labels.length - 1] || (isMobile ? "行业" : "行业筛选")
}
disabled={industryLoading} disabled={industryLoading}
style={{ minWidth: isMobile ? 70 : 80 }} style={{ minWidth: isMobile ? 70 : 80 }}
suffixIcon={null} suffixIcon={null}
className="transparent-cascader" className="transparent-cascader"
/> />
{/* 事件等级 */}
<AntSelect <AntSelect
mode="multiple" mode="multiple"
value={importance} value={importance}
onChange={handleImportanceChange} onChange={handleImportanceChange}
style={{ minWidth: isMobile ? 100 : 120 }} style={{ minWidth: isMobile ? 100 : 120 }}
placeholder={ placeholder={
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <span style={{ display: "flex", alignItems: "center", gap: 4 }}>
<ThunderboltOutlined style={{ fontSize: 12 }} /> <ThunderboltOutlined style={{ fontSize: 12 }} />
{isMobile ? '等级' : '事件等级'} {isMobile ? "等级" : "事件等级"}
</span> </span>
} }
maxTagCount={0} maxTagCount={0}
maxTagPlaceholder={(omittedValues) => isMobile ? `${omittedValues.length}` : `已选 ${omittedValues.length}`} maxTagPlaceholder={(omittedValues) =>
isMobile ? `${omittedValues.length}` : `已选 ${omittedValues.length}`
}
className="bracket-select" className="bracket-select"
> >
{IMPORTANCE_OPTIONS.map(opt => ( {IMPORTANCE_OPTIONS.map((opt) => (
<Option key={opt.value} value={opt.value}>{opt.label}</Option> <Option key={opt.value} value={opt.value}>
{opt.label}
</Option>
))} ))}
</AntSelect> </AntSelect>
</Space> </Space>
{/* 右侧排序和重置 */}
<Space size={isMobile ? 4 : 8}> <Space size={isMobile ? 4 : 8}>
{/* 排序 */}
<AntSelect <AntSelect
value={sort} value={sort}
onChange={handleSortChange} onChange={handleSortChange}
style={{ minWidth: isMobile ? 55 : 120 }} style={{ minWidth: isMobile ? 55 : 120 }}
className="bracket-select" className="bracket-select"
> >
{SORT_OPTIONS.map(opt => ( {SORT_OPTIONS.map((opt) => (
<Option key={opt.value} value={opt.value}> <Option key={opt.value} value={opt.value}>
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <span style={{ display: "flex", alignItems: "center", gap: 4 }}>
<SortAscendingOutlined style={{ fontSize: 12 }} /> <SortAscendingOutlined style={{ fontSize: 12 }} />
{isMobile ? opt.mobileLabel : opt.label} {isMobile ? opt.mobileLabel : opt.label}
</span> </span>
@@ -622,14 +525,13 @@ const CompactSearchBox = ({
))} ))}
</AntSelect> </AntSelect>
{/* 重置按钮 */}
<Button <Button
icon={<ReloadOutlined />} icon={<ReloadOutlined />}
onClick={handleReset} onClick={handleReset}
type="text" type="text"
style={{ color: PROFESSIONAL_COLORS.text.secondary }} style={{ color: PROFESSIONAL_COLORS.text.secondary }}
> >
{!isMobile && '重置筛选'} {!isMobile && "重置筛选"}
</Button> </Button>
</Space> </Space>
</Flex> </Flex>

View File

@@ -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级" },
];

View File

@@ -0,0 +1,4 @@
// CompactSearchBox 模块导出
export { SORT_OPTIONS, IMPORTANCE_OPTIONS } from "./constants";
export { findIndustryPath, inferTimeRangeFromFilters, buildFilterParams } from "./utils";

View File

@@ -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;
};