feat: 调整搜索框UI
This commit is contained in:
@@ -4,30 +4,49 @@
|
||||
import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
||||
import {
|
||||
Input, Cascader, Button, Space, Tag, AutoComplete, Select as AntSelect,
|
||||
Tooltip
|
||||
Tooltip, Divider, Flex
|
||||
} from 'antd';
|
||||
import {
|
||||
SearchOutlined, CloseCircleOutlined, StockOutlined, FilterOutlined,
|
||||
CalendarOutlined, SortAscendingOutlined
|
||||
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 { stockService } from '../../../services/stockService';
|
||||
import { logger } from '../../../utils/logger';
|
||||
import { fetchIndustryData, selectIndustryData, selectIndustryLoading } from '@store/slices/industrySlice';
|
||||
import { stockService } from '@services/stockService';
|
||||
import { logger } from '@utils/logger';
|
||||
import TradingTimeFilter from './TradingTimeFilter';
|
||||
import { PROFESSIONAL_COLORS } from '../../../constants/professionalTheme';
|
||||
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme';
|
||||
import './CompactSearchBox.css';
|
||||
|
||||
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 = {}
|
||||
trackingFunctions = {},
|
||||
isMobile = false
|
||||
}) => {
|
||||
// 状态
|
||||
const [stockOptions, setStockOptions] = useState([]);
|
||||
@@ -420,19 +439,21 @@ const CompactSearchBox = ({
|
||||
dispatch(fetchIndustryData());
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: window.innerWidth < 768 ? '12px 16px' : '16px 20px',
|
||||
background: PROFESSIONAL_COLORS.background.card,
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3), 0 0 20px rgba(255, 195, 0, 0.1)',
|
||||
border: `1px solid ${PROFESSIONAL_COLORS.border.default}`,
|
||||
backdropFilter: 'blur(10px)'
|
||||
}}>
|
||||
{/* 单行紧凑布局 - 移动端自动换行 */}
|
||||
<Space wrap style={{ width: '100%' }} size={window.innerWidth < 768 ? 'small' : 'medium'}>
|
||||
{/* 搜索框 */}
|
||||
<div style={{ padding: 0, background: 'transparent' }}>
|
||||
{/* 第一行:搜索框 + 日期筛选 */}
|
||||
<Flex
|
||||
align="center"
|
||||
gap={isMobile ? 8 : 12}
|
||||
style={{
|
||||
background: 'rgba(255, 255, 255, 0.03)',
|
||||
border: `1px solid ${PROFESSIONAL_COLORS.border.light}`,
|
||||
borderRadius: '24px',
|
||||
padding: isMobile ? '2px 4px' : '8px 16px',
|
||||
marginBottom: isMobile ? 8 : 12
|
||||
}}
|
||||
>
|
||||
{/* 搜索框 - flex: 1 占满剩余空间 */}
|
||||
<AutoComplete
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
@@ -440,46 +461,57 @@ const CompactSearchBox = ({
|
||||
onSelect={handleStockSelect}
|
||||
onFocus={onSearchFocus}
|
||||
options={stockOptions}
|
||||
placeholder="搜索股票/话题..."
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleMainSearch();
|
||||
}
|
||||
}}
|
||||
style={{ width: window.innerWidth < 768 ? '100%' : 240, minWidth: window.innerWidth < 768 ? 0 : 240 }}
|
||||
style={{ flex: 1, minWidth: isMobile ? 100 : 200 }}
|
||||
className="gold-placeholder"
|
||||
>
|
||||
<Input
|
||||
prefix={<SearchOutlined style={{ color: PROFESSIONAL_COLORS.gold[500] }} />}
|
||||
placeholder="搜索股票/话题..."
|
||||
style={{
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${PROFESSIONAL_COLORS.border.default}`,
|
||||
boxShadow: `0 2px 8px rgba(255, 195, 0, 0.1)`,
|
||||
background: PROFESSIONAL_COLORS.background.secondary,
|
||||
color: PROFESSIONAL_COLORS.text.primary
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: PROFESSIONAL_COLORS.text.primary,
|
||||
boxShadow: 'none'
|
||||
}}
|
||||
/>
|
||||
</AutoComplete>
|
||||
|
||||
{/* 时间筛选 */}
|
||||
<Tooltip title="时间筛选">
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||
<CalendarOutlined style={{ color: PROFESSIONAL_COLORS.gold[500], fontSize: 12 }} />
|
||||
<TradingTimeFilter
|
||||
value={tradingTimeRange?.key || null}
|
||||
onChange={handleTradingTimeChange}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/* 分隔线 - H5 时隐藏 */}
|
||||
{!isMobile && <Divider type="vertical" style={{ height: 24, margin: '0 8px', borderColor: 'rgba(255,255,255,0.15)' }} />}
|
||||
|
||||
{/* 行业筛选 */}
|
||||
<Tooltip title="行业分类">
|
||||
{/* 日期筛选按钮组 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 0 }}>
|
||||
<CalendarOutlined style={{ color: PROFESSIONAL_COLORS.gold[500], fontSize: 14, marginRight: 8 }} />
|
||||
<TradingTimeFilter
|
||||
value={tradingTimeRange?.key || null}
|
||||
onChange={handleTradingTimeChange}
|
||||
compact={!isMobile}
|
||||
mobile={isMobile}
|
||||
/>
|
||||
</div>
|
||||
</Flex>
|
||||
|
||||
{/* 第二行:筛选条件 */}
|
||||
<Flex justify="space-between" align="center">
|
||||
{/* 左侧筛选 */}
|
||||
<Space size={isMobile ? 4 : 8}>
|
||||
{/* 行业筛选 */}
|
||||
<Cascader
|
||||
value={industryValue}
|
||||
onChange={handleIndustryChange}
|
||||
onFocus={handleCascaderFocus}
|
||||
options={industryData || []}
|
||||
placeholder="行业"
|
||||
placeholder={
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<FilterOutlined style={{ fontSize: 12 }} />
|
||||
{isMobile ? '行业' : '行业筛选'}
|
||||
</span>
|
||||
}
|
||||
changeOnSelect
|
||||
showSearch={{
|
||||
filter: (inputValue, path) =>
|
||||
@@ -489,145 +521,65 @@ const CompactSearchBox = ({
|
||||
}}
|
||||
allowClear
|
||||
expandTrigger="hover"
|
||||
displayRender={(labels) => labels[labels.length - 1] || '行业'}
|
||||
displayRender={(labels) => labels[labels.length - 1] || (isMobile ? '行业' : '行业筛选')}
|
||||
disabled={industryLoading}
|
||||
style={{
|
||||
width: window.innerWidth < 768 ? '100%' : 120,
|
||||
minWidth: window.innerWidth < 768 ? 0 : 120,
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
suffixIcon={<FilterOutlined style={{ fontSize: 14, color: PROFESSIONAL_COLORS.gold[500] }} />}
|
||||
style={{ minWidth: isMobile ? 70 : 80 }}
|
||||
suffixIcon={null}
|
||||
className="transparent-cascader"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* 重要性筛选 */}
|
||||
<Tooltip title="事件等级筛选">
|
||||
{/* 事件等级 */}
|
||||
<AntSelect
|
||||
mode="multiple"
|
||||
value={importance}
|
||||
onChange={handleImportanceChange}
|
||||
style={{
|
||||
width: window.innerWidth < 768 ? '100%' : 120,
|
||||
minWidth: window.innerWidth < 768 ? 0 : 120,
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
placeholder="事件等级"
|
||||
style={{ minWidth: isMobile ? 100 : 120 }}
|
||||
placeholder={
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<ThunderboltOutlined style={{ fontSize: 12 }} />
|
||||
{isMobile ? '等级' : '事件等级'}
|
||||
</span>
|
||||
}
|
||||
maxTagCount={0}
|
||||
maxTagPlaceholder={(omittedValues) => `已选 ${omittedValues.length} 项`}
|
||||
maxTagPlaceholder={(omittedValues) => isMobile ? `${omittedValues.length}项` : `已选 ${omittedValues.length} 项`}
|
||||
className="bracket-select"
|
||||
>
|
||||
<Option value="S">S级</Option>
|
||||
<Option value="A">A级</Option>
|
||||
<Option value="B">B级</Option>
|
||||
<Option value="C">C级</Option>
|
||||
{IMPORTANCE_OPTIONS.map(opt => (
|
||||
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
|
||||
))}
|
||||
</AntSelect>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
||||
{/* 排序 */}
|
||||
<Tooltip title="排序方式">
|
||||
{/* 右侧排序和重置 */}
|
||||
<Space size={isMobile ? 4 : 8}>
|
||||
{/* 排序 */}
|
||||
<AntSelect
|
||||
value={sort}
|
||||
onChange={handleSortChange}
|
||||
style={{
|
||||
width: window.innerWidth < 768 ? '100%' : 130,
|
||||
minWidth: window.innerWidth < 768 ? 0 : 130,
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
suffixIcon={<SortAscendingOutlined style={{ fontSize: 14, color: PROFESSIONAL_COLORS.gold[500] }} />}
|
||||
style={{ minWidth: isMobile ? 55 : 120 }}
|
||||
className="bracket-select"
|
||||
>
|
||||
<Option value="new">⏰ 最新</Option>
|
||||
<Option value="hot">🔥 最热</Option>
|
||||
<Option value="importance">⭐ 重要性</Option>
|
||||
<Option value="returns_avg">📊 平均收益</Option>
|
||||
<Option value="returns_week">📈 周收益</Option>
|
||||
{SORT_OPTIONS.map(opt => (
|
||||
<Option key={opt.value} value={opt.value}>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<SortAscendingOutlined style={{ fontSize: 12 }} />
|
||||
{isMobile ? opt.mobileLabel : opt.label}
|
||||
</span>
|
||||
</Option>
|
||||
))}
|
||||
</AntSelect>
|
||||
</Tooltip>
|
||||
|
||||
{/* 重置按钮 */}
|
||||
<Tooltip title="重置所有筛选">
|
||||
{/* 重置按钮 */}
|
||||
<Button
|
||||
icon={<CloseCircleOutlined />}
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleReset}
|
||||
danger
|
||||
type="primary"
|
||||
style={{
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 8px rgba(255, 77, 79, 0.2)'
|
||||
}}
|
||||
type="text"
|
||||
style={{ color: PROFESSIONAL_COLORS.text.secondary }}
|
||||
>
|
||||
重置
|
||||
{!isMobile && '重置筛选'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
||||
{/* 激活的筛选标签(如果有的话) */}
|
||||
{(inputValue || industryValue.length > 0 || importance.length > 0 || tradingTimeRange || sort !== 'new') && (
|
||||
<div style={{ marginTop: 8, display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
||||
{inputValue && (
|
||||
<Tag closable onClose={() => {
|
||||
setInputValue('');
|
||||
const params = buildFilterParams({ q: '' });
|
||||
triggerSearch(params);
|
||||
}} color="blue">
|
||||
搜索: {inputValue}
|
||||
</Tag>
|
||||
)}
|
||||
{tradingTimeRange && (
|
||||
<Tag closable onClose={() => {
|
||||
setTradingTimeRange(null);
|
||||
const params = buildFilterParams({
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
recent_days: ''
|
||||
});
|
||||
triggerSearch(params);
|
||||
}} color="green">
|
||||
{tradingTimeRange.label}
|
||||
</Tag>
|
||||
)}
|
||||
{industryValue.length > 0 && industryData && (
|
||||
<Tag closable onClose={() => {
|
||||
setIndustryValue([]);
|
||||
const params = buildFilterParams({ industry_code: '' });
|
||||
triggerSearch(params);
|
||||
}} color="orange">
|
||||
行业: {(() => {
|
||||
const findLabel = (code, data) => {
|
||||
for (const item of data) {
|
||||
if (code.startsWith(item.value)) {
|
||||
if (item.value === code) {
|
||||
return item.label;
|
||||
} else {
|
||||
return findLabel(code, item.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const lastLevelCode = industryValue[industryValue.length - 1];
|
||||
return findLabel(lastLevelCode, industryData);
|
||||
})()}
|
||||
</Tag>
|
||||
)}
|
||||
{importance.length > 0 && (
|
||||
<Tag closable onClose={() => {
|
||||
setImportance([]);
|
||||
const params = buildFilterParams({ importance: 'all' });
|
||||
triggerSearch(params);
|
||||
}} color="purple">
|
||||
重要性: {importance.map(imp => ({ 'S': '极高', 'A': '高', 'B': '中', 'C': '低' }[imp] || imp)).join(', ')}
|
||||
</Tag>
|
||||
)}
|
||||
{sort && sort !== 'new' && (
|
||||
<Tag closable onClose={() => {
|
||||
setSort('new');
|
||||
const params = buildFilterParams({ sort: 'new' });
|
||||
triggerSearch(params);
|
||||
}} color="cyan">
|
||||
排序: {({ 'hot': '最热', 'importance': '重要性', 'returns_avg': '平均收益', 'returns_week': '周收益' }[sort] || sort)}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user