feat: 添加行业筛选器Box
This commit is contained in:
405
src/views/Community/components/UnifiedSearchBox.js
Normal file
405
src/views/Community/components/UnifiedSearchBox.js
Normal 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;
|
||||
Reference in New Issue
Block a user