feat: 添加行业筛选器Box

This commit is contained in:
zdl
2025-10-25 18:23:20 +08:00
parent 506f89e64e
commit 0a0d617b20

View File

@@ -0,0 +1,405 @@
// src/views/Community/components/UnifiedSearchBox.js
// 搜索组件:三行布局(主搜索 + 热门概念 + 筛选区)
import React, { useState, useMemo, useEffect } 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 locale from 'antd/es/date-picker/locale/zh_CN';
import { useIndustry } from '../../../contexts/IndustryContext';
import { stockService } from '../../../services/stockService';
import { logger } from '../../../utils/logger';
import PopularKeywords from './PopularKeywords';
const { RangePicker } = DatePicker;
const { Option } = AntSelect;
const UnifiedSearchBox = ({
onSearch,
popularKeywords = [],
filters = {},
loading = false
}) => {
const [searchValue, setSearchValue] = useState(''); // 统一搜索值
const [isStockSearch, setIsStockSearch] = useState(false); // 是否股票搜索
const [stockOptions, setStockOptions] = useState([]); // 股票下拉选项列表
const [allStocks, setAllStocks] = useState([]); // 所有股票数据
const [industryValue, setIndustryValue] = useState([]);
// 使用全局行业数据
const { industryData, loadIndustryData, loading: industryLoading } = useIndustry();
// 加载所有股票数据
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();
}
};
// ⚡ 提取 filters 中的原始值,避免对象引用导致无限循环
const stockCode = filters.stock_code;
const q = filters.q;
const industryCode = filters.industry_code;
// 初始化:从 URL 恢复状态
React.useEffect(() => {
if (stockCode) {
setSearchValue(stockCode);
setIsStockSearch(true);
} else if (q) {
setSearchValue(q);
setIsStockSearch(false);
}
if (industryCode) {
// TODO: 从 industry_code 恢复 industryValue 需要查找对应路径
}
}, [stockCode, q, industryCode]); // ⚡ 只依赖原始值,不依赖整个 filters 对象
// 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: (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<StockOutlined style={{ color: '#1890ff' }} />
<span style={{ fontWeight: 500, color: '#333' }}>{stock.code}</span>
<span style={{ color: '#666' }}>{stock.name}</span>
</div>
),
// 保存完整的股票信息,用于选中后显示
stockInfo: stock
}));
setStockOptions(options);
logger.debug('UnifiedSearchBox', '股票模糊搜索', {
query: value,
resultCount: options.length
});
};
// 选中股票(从下拉选择)
const handleStockSelect = (value, option) => {
const stockInfo = option.stockInfo;
if (stockInfo) {
// 显示格式:股票代码 + 股票名称
const displayValue = `${stockInfo.code} ${stockInfo.name}`;
setSearchValue(displayValue);
setIsStockSearch(true);
logger.debug('UnifiedSearchBox', '选中股票', {
code: stockInfo.code,
name: stockInfo.name
});
}
};
// 日期范围变化
const handleDateRangeChange = (dates) => {
// 这里不直接调用 onSearch等用户点击搜索按钮
logger.debug('UnifiedSearchBox', '日期范围变化', { dates });
};
// 重要性变化
const handleImportanceChange = (value) => {
logger.debug('UnifiedSearchBox', '重要性变化', { value });
};
// 排序变化
const handleSortChange = (value) => {
// 排序直接生效
onSearch({ sort: value, page: 1 });
};
// 主搜索(点击搜索按钮或回车)
const handleMainSearch = () => {
// 直接输入文本(未选择下拉股票)时,作为话题搜索
if (!isStockSearch && searchValue) {
logger.debug('UnifiedSearchBox', '话题搜索', { topic: searchValue });
}
handleApplyFilters();
};
// 处理输入变化
const handleInputChange = (value) => {
setSearchValue(value);
// 输入变化时重置股票搜索标记(只有从下拉选择才是股票搜索)
if (isStockSearch) {
setIsStockSearch(false);
logger.debug('UnifiedSearchBox', '切换为话题搜索模式');
}
};
// 应用筛选
const handleApplyFilters = () => {
const params = { page: 1 };
if (isStockSearch && searchValue) {
// 股票搜索模式:从下拉选择的股票
// 提取股票代码searchValue 格式:"000001 平安银行"
const stockCode = searchValue.split(' ')[0];
params.stock_code = stockCode;
params.q = '';
params.industry_code = '';
params.industry_classification = '';
logger.debug('UnifiedSearchBox', '应用股票筛选', { stockCode });
} else {
// 话题搜索模式:直接输入的文本
params.q = searchValue || '';
params.stock_code = '';
if (industryValue && industryValue.length > 0) {
params.industry_code = industryValue[industryValue.length - 1];
params.industry_classification = industryValue[0];
} else {
params.industry_code = '';
params.industry_classification = '';
}
logger.debug('UnifiedSearchBox', '应用话题筛选', { topic: searchValue });
}
onSearch(params);
};
// 重置筛选
const handleReset = () => {
setSearchValue('');
setIsStockSearch(false);
setStockOptions([]);
setIndustryValue([]);
onSearch({
q: '',
stock_code: '',
industry_code: '',
industry_classification: '',
sort: 'new',
importance: 'all',
page: 1
});
};
// 生成已选条件标签
const filterTags = useMemo(() => {
const tags = [];
if (isStockSearch && searchValue) {
tags.push({ key: 'stock', label: `股票: ${searchValue}` });
} else if (!isStockSearch && searchValue) {
tags.push({ key: 'topic', label: `话题: ${searchValue}` });
}
if (industryValue && industryValue.length > 0) {
const industryLabel = industryValue.slice(1).join(' > ');
tags.push({ key: 'industry', label: `行业: ${industryLabel}` });
}
return tags;
}, [searchValue, isStockSearch, industryValue]);
// 移除单个标签
const handleRemoveTag = (key) => {
if (key === 'topic' || key === 'stock') {
setSearchValue('');
setIsStockSearch(false);
} else if (key === 'industry') {
setIndustryValue([]);
}
setTimeout(handleApplyFilters, 100);
};
return (
<Card>
{/* 第一行:主搜索框 */}
<Space.Compact style={{ width: '100%', marginBottom: 12 }} size="large">
<SearchOutlined style={{
fontSize: 20,
padding: '8px 12px',
background: '#f5f5f5',
borderRadius: '6px 0 0 6px',
display: 'flex',
alignItems: 'center',
color: '#666'
}} />
<AutoComplete
value={searchValue}
onChange={handleInputChange}
onSearch={handleSearch}
onSelect={handleStockSelect}
options={stockOptions}
placeholder="请输入股票代码/股票名称/相关话题"
onPressEnter={handleMainSearch}
style={{ flex: 1 }}
size="large"
notFoundContent={searchValue && stockOptions.length === 0 ? "未找到匹配的股票" : null}
/>
<Button
type="primary"
onClick={handleMainSearch}
loading={loading}
size="large"
style={{ minWidth: 80 }}
>
搜索
</Button>
</Space.Compact>
{/* 第二行:热门概念 */}
<div style={{ marginBottom: 12 }}>
<PopularKeywords />
</div>
{/* 第三行:筛选器 + 排序 */}
<Space style={{ width: '100%', justifyContent: 'space-between' }} size="middle">
{/* 左侧:筛选器组 */}
<Space size="middle" wrap>
<span style={{ fontSize: 14, color: '#666', fontWeight: 'bold' }}>筛选:</span>
{/* 行业分类 */}
<Cascader
value={industryValue}
onChange={setIndustryValue}
onFocus={handleCascaderFocus}
options={industryData || []}
placeholder="行业分类"
changeOnSelect
showSearch={{
filter: (inputValue, path) =>
path.some(option =>
option.label.toLowerCase().includes(inputValue.toLowerCase())
)
}}
allowClear
expandTrigger="hover"
displayRender={(labels) => labels.join(' > ')}
disabled={industryLoading}
loading={industryLoading}
style={{ width: 200 }}
size="middle"
/>
{/* 日期范围 */}
<RangePicker
onChange={handleDateRangeChange}
locale={locale}
placeholder={['开始日期', '结束日期']}
style={{ width: 240 }}
size="middle"
/>
{/* 重要性 */}
<Space size="small">
<span style={{ fontSize: 14, color: '#666' }}>重要性:</span>
<AntSelect
defaultValue="all"
onChange={handleImportanceChange}
style={{ width: 100 }}
size="middle"
>
<Option value="all">全部</Option>
<Option value="S">S级</Option>
<Option value="A">A级</Option>
<Option value="B">B级</Option>
<Option value="C">C级</Option>
</AntSelect>
</Space>
{/* 重置按钮 - 现代化设计 */}
<Button
icon={<CloseCircleOutlined />}
onClick={handleReset}
size="middle"
style={{
borderRadius: 6,
border: '1px solid #d9d9d9',
backgroundColor: '#fff',
color: '#666',
fontWeight: 500,
padding: '4px 12px',
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)';
}}
>
重置
</Button>
</Space>
{/* 右侧:排序 */}
<Space size="small">
<span style={{ fontSize: 14, color: '#666' }}>排序:</span>
<AntSelect
defaultValue="new"
onChange={handleSortChange}
style={{ width: 120 }}
size="middle"
>
<Option value="new">最新</Option>
<Option value="hot">最热</Option>
<Option value="importance">重要性</Option>
</AntSelect>
</Space>
</Space>
{/* 已选条件标签 */}
{filterTags.length > 0 && (
<Space size={[8, 8]} wrap style={{ marginTop: 12 }}>
{filterTags.map(tag => (
<Tag
key={tag.key}
closable
onClose={() => handleRemoveTag(tag.key)}
color="blue"
>
{tag.label}
</Tag>
))}
</Space>
)}
</Card>
);
};
export default UnifiedSearchBox;