feat: 调整搜索框UI

This commit is contained in:
zdl
2025-11-26 19:33:00 +08:00
parent 24ddfcd4b5
commit 805d446775
3 changed files with 324 additions and 163 deletions

View File

@@ -0,0 +1,80 @@
/* CompactSearchBox.css */
/* 紧凑版搜索和筛选组件样式 */
/* 搜索框 placeholder 白色 - 全覆盖选择器 */
.gold-placeholder input::placeholder,
.gold-placeholder input[type="text"]::placeholder,
.gold-placeholder .ant-input::placeholder,
.gold-placeholder .ant-input-affix-wrapper input::placeholder,
.gold-placeholder .ant-select-selection-search-input::placeholder,
.gold-placeholder .ant-input-affix-wrapper .ant-input::placeholder {
color: #FFFFFF !important;
opacity: 0.8 !important;
}
/* AutoComplete placeholder - 关键选择器 */
.gold-placeholder .ant-select-selection-placeholder {
color: #FFFFFF !important;
opacity: 0.8 !important;
}
.gold-placeholder .ant-input-affix-wrapper .ant-input,
.gold-placeholder .ant-input {
color: #FFFFFF !important;
}
.gold-placeholder .ant-input-affix-wrapper {
background: transparent !important;
}
/* 透明下拉框样式 */
.transparent-select .ant-select-selector,
.transparent-cascader .ant-select-selector {
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
/* 行业筛选宽度自适应,减少间距 */
.transparent-cascader {
width: auto !important;
}
.transparent-cascader .ant-select-selector {
padding-right: 8px !important;
min-width: unset !important;
}
/* 行业筛选 Cascader placeholder 白色 */
.transparent-select .ant-select-selection-placeholder,
.transparent-cascader .ant-select-selection-placeholder,
.transparent-cascader input::placeholder,
.transparent-cascader .ant-cascader-input::placeholder {
color: #FFFFFF !important;
}
.transparent-cascader .ant-cascader-input {
background: transparent !important;
}
/* 行业筛选 Cascader 选中值白色 */
.transparent-cascader .ant-select-selection-item,
.transparent-cascader .ant-cascader-picker-label {
color: #FFFFFF !important;
}
/* 方括号样式下拉框 - 无边框 */
.bracket-select .ant-select-selector {
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
.bracket-select .ant-select-selection-item,
.bracket-select .ant-select-selection-placeholder {
color: #FFFFFF !important;
}
.bracket-select .ant-select-arrow {
color: rgba(255, 255, 255, 0.65) !important;
}

View File

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

View File

@@ -1,20 +1,25 @@
// src/views/Community/components/TradingTimeFilter.js // src/views/Community/components/TradingTimeFilter.js
// 交易时段智能筛选组件 // 交易时段智能筛选组件
import React, { useState, useMemo, useEffect } from 'react'; import React, { useState, useMemo, useEffect } from 'react';
import { Space, Button, Tag, Tooltip, DatePicker, Popover } from 'antd'; import { Space, Button, Tag, Tooltip, DatePicker, Popover, Select } from 'antd';
import { ClockCircleOutlined, CalendarOutlined } from '@ant-design/icons'; import { ClockCircleOutlined, CalendarOutlined, FilterOutlined } from '@ant-design/icons';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import locale from 'antd/es/date-picker/locale/zh_CN'; import locale from 'antd/es/date-picker/locale/zh_CN';
import { logger } from '../../../utils/logger'; import { logger } from '@utils/logger';
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme';
const { RangePicker } = DatePicker; const { RangePicker } = DatePicker;
const { Option } = Select;
/** /**
* 交易时段筛选组件 * 交易时段筛选组件
* @param {string} value - 当前选中的 key受控 * @param {string} value - 当前选中的 key受控
* @param {Function} onChange - 时间范围变化回调 (timeConfig) => void * @param {Function} onChange - 时间范围变化回调 (timeConfig) => void
* @param {boolean} compact - 是否使用紧凑模式PC 端搜索栏内使用)
* @param {boolean} mobile - 是否使用移动端模式(下拉选择)
*/ */
const TradingTimeFilter = ({ value, onChange }) => { const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false }) => {
const [selectedKey, setSelectedKey] = useState(null); const [selectedKey, setSelectedKey] = useState(null);
const [customRangeVisible, setCustomRangeVisible] = useState(false); const [customRangeVisible, setCustomRangeVisible] = useState(false);
const [customRange, setCustomRange] = useState(null); const [customRange, setCustomRange] = useState(null);
@@ -266,7 +271,39 @@ const TradingTimeFilter = ({ value, onChange }) => {
} }
}; };
// 渲染按钮 // 渲染紧凑模式按钮PC 端搜索栏内使用,文字按钮 + | 分隔符)
const renderCompactButton = (config, showDivider = true) => {
const isSelected = selectedKey === config.key;
const fullTooltip = config.timeHint ? `${config.tooltip} · ${config.timeHint}` : config.tooltip;
return (
<React.Fragment key={config.key}>
<Tooltip title={fullTooltip}>
<span
onClick={() => handleButtonClick(config)}
style={{
cursor: 'pointer',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '13px',
fontWeight: isSelected ? 600 : 400,
color: isSelected ? PROFESSIONAL_COLORS.gold[500] : PROFESSIONAL_COLORS.text.secondary,
background: isSelected ? 'rgba(255, 195, 0, 0.15)' : 'transparent',
transition: 'all 0.2s ease',
whiteSpace: 'nowrap',
}}
>
{config.label}
</span>
</Tooltip>
{showDivider && (
<span style={{ color: 'rgba(255, 255, 255, 0.2)', margin: '0 2px' }}>|</span>
)}
</React.Fragment>
);
};
// 渲染按钮(默认模式)
const renderButton = (config) => { const renderButton = (config) => {
const isSelected = selectedKey === config.key; const isSelected = selectedKey === config.key;
@@ -321,6 +358,98 @@ const TradingTimeFilter = ({ value, onChange }) => {
</div> </div>
); );
// 移动端模式:下拉选择器
if (mobile) {
const allButtons = [...timeRangeConfig.dynamic, ...timeRangeConfig.fixed];
const handleMobileSelect = (key) => {
if (key === selectedKey) {
// 取消选中
setSelectedKey(null);
onChange(null);
} else {
const config = allButtons.find(b => b.key === key);
if (config) {
handleButtonClick(config);
}
}
};
return (
<Select
value={selectedKey}
onChange={handleMobileSelect}
placeholder={
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<FilterOutlined style={{ fontSize: 12 }} />
筛选
</span>
}
allowClear
onClear={() => {
setSelectedKey(null);
onChange(null);
}}
style={{ minWidth: 80 }}
className="transparent-select"
popupMatchSelectWidth={false}
>
{allButtons.map(config => (
<Option key={config.key} value={config.key}>
{config.label}
</Option>
))}
</Select>
);
}
// 紧凑模式PC 端搜索栏内的样式
if (compact) {
// 合并所有按钮配置
const allButtons = [...timeRangeConfig.dynamic, ...timeRangeConfig.fixed];
return (
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'nowrap' }}>
{allButtons.map((config, index) =>
renderCompactButton(config, index < allButtons.length - 1)
)}
{/* 更多时间 */}
<span style={{ color: 'rgba(255, 255, 255, 0.2)', margin: '0 2px' }}>|</span>
<Popover
content={customRangeContent}
title="选择自定义时间范围"
trigger="click"
open={customRangeVisible}
onOpenChange={setCustomRangeVisible}
placement="bottomLeft"
>
<Tooltip title="自定义时间范围">
<span
style={{
cursor: 'pointer',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '13px',
fontWeight: selectedKey === 'custom' ? 600 : 400,
color: selectedKey === 'custom' ? PROFESSIONAL_COLORS.gold[500] : PROFESSIONAL_COLORS.text.secondary,
background: selectedKey === 'custom' ? 'rgba(255, 195, 0, 0.15)' : 'transparent',
transition: 'all 0.2s ease',
whiteSpace: 'nowrap',
display: 'flex',
alignItems: 'center',
gap: '4px',
}}
>
<CalendarOutlined style={{ fontSize: 12 }} />
更多
</span>
</Tooltip>
</Popover>
</div>
);
}
// 默认模式:移动端/独立使用
return ( return (
<Space wrap size={[8, 8]} style={{ display: 'flex', alignItems: 'flex-start' }}> <Space wrap size={[8, 8]} style={{ display: 'flex', alignItems: 'flex-start' }}>
{/* 动态按钮(根据时段显示多个) */} {/* 动态按钮(根据时段显示多个) */}