refactor(TradingTimeFilter): 提取常量和工具函数到子模块
- constants.js: TIME_BOUNDARIES, TRADING_SESSIONS 时间边界配置 - utils.js: getCurrentTradingSession, getPrevTradingDay, generateTimeRangeConfig, disabledDate/Time - index.js: 模块统一导出 主文件从 ~440 行精简到 ~260 行 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,239 +1,84 @@
|
|||||||
// src/views/Community/components/TradingTimeFilter.js
|
// src/views/Community/components/TradingTimeFilter.js
|
||||||
// 交易时段智能筛选组件
|
// 交易时段智能筛选组件
|
||||||
import React, { useState, useMemo, useEffect } from 'react';
|
|
||||||
import { Space, Button, Tag, Tooltip, DatePicker, Popover, Select } from 'antd';
|
import React, { useState, useMemo, useEffect } from "react";
|
||||||
import { ClockCircleOutlined, CalendarOutlined, FilterOutlined } from '@ant-design/icons';
|
import {
|
||||||
import dayjs from 'dayjs';
|
Space,
|
||||||
import locale from 'antd/es/date-picker/locale/zh_CN';
|
Button,
|
||||||
import { logger } from '@utils/logger';
|
Tag,
|
||||||
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme';
|
Tooltip,
|
||||||
import tradingDayUtils from '@utils/tradingDayUtils';
|
DatePicker,
|
||||||
|
Popover,
|
||||||
|
Select,
|
||||||
|
} from "antd";
|
||||||
|
import {
|
||||||
|
ClockCircleOutlined,
|
||||||
|
CalendarOutlined,
|
||||||
|
FilterOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import locale from "antd/es/date-picker/locale/zh_CN";
|
||||||
|
import { logger } from "@utils/logger";
|
||||||
|
import { PROFESSIONAL_COLORS } from "@constants/professionalTheme";
|
||||||
|
|
||||||
|
// 模块化导入
|
||||||
|
import {
|
||||||
|
generateTimeRangeConfig,
|
||||||
|
disabledDate,
|
||||||
|
disabledTime,
|
||||||
|
} from "./TradingTimeFilter/utils";
|
||||||
|
|
||||||
const { RangePicker } = DatePicker;
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 交易时段筛选组件
|
* 交易时段筛选组件
|
||||||
* @param {string} value - 当前选中的 key(受控)
|
|
||||||
* @param {Function} onChange - 时间范围变化回调 (timeConfig) => void
|
|
||||||
* @param {boolean} compact - 是否使用紧凑模式(PC 端搜索栏内使用)
|
|
||||||
* @param {boolean} mobile - 是否使用移动端模式(下拉选择)
|
|
||||||
*/
|
*/
|
||||||
const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false }) => {
|
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);
|
||||||
|
|
||||||
// 监听外部 value 变化,同步内部状态
|
// 监听外部 value 变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
// 外部重置,清空内部状态
|
|
||||||
setSelectedKey(null);
|
setSelectedKey(null);
|
||||||
setCustomRange(null);
|
setCustomRange(null);
|
||||||
logger.debug('TradingTimeFilter', '外部重置,清空选中状态');
|
logger.debug("TradingTimeFilter", "外部重置,清空选中状态");
|
||||||
} else {
|
} else {
|
||||||
// 外部选中值变化,同步内部状态
|
|
||||||
setSelectedKey(value);
|
setSelectedKey(value);
|
||||||
logger.debug('TradingTimeFilter', '外部value变化,同步内部状态', { value });
|
logger.debug("TradingTimeFilter", "外部value变化,同步内部状态", { value });
|
||||||
}
|
}
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
// 获取当前交易时段
|
|
||||||
const getCurrentTradingSession = () => {
|
|
||||||
const now = dayjs();
|
|
||||||
const hour = now.hour();
|
|
||||||
const minute = now.minute();
|
|
||||||
const currentMinutes = hour * 60 + minute;
|
|
||||||
|
|
||||||
// 转换为分钟数便于比较
|
|
||||||
const PRE_MARKET_END = 9 * 60 + 30; // 09:30
|
|
||||||
const MORNING_END = 11 * 60 + 30; // 11:30
|
|
||||||
const AFTERNOON_START = 13 * 60; // 13:00
|
|
||||||
const MARKET_CLOSE = 15 * 60; // 15:00
|
|
||||||
|
|
||||||
if (currentMinutes < PRE_MARKET_END) {
|
|
||||||
return 'pre-market'; // 盘前
|
|
||||||
} else if (currentMinutes < MORNING_END) {
|
|
||||||
return 'morning'; // 早盘
|
|
||||||
} else if (currentMinutes < AFTERNOON_START) {
|
|
||||||
return 'lunch'; // 午休
|
|
||||||
} else if (currentMinutes < MARKET_CLOSE) {
|
|
||||||
return 'afternoon'; // 午盘
|
|
||||||
} else {
|
|
||||||
return 'after-hours'; // 盘后
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取时间范围配置
|
// 获取时间范围配置
|
||||||
const timeRangeConfig = useMemo(() => {
|
const timeRangeConfig = useMemo(() => generateTimeRangeConfig(), []);
|
||||||
const session = getCurrentTradingSession();
|
|
||||||
const now = dayjs();
|
|
||||||
|
|
||||||
// 今日关键时间点
|
|
||||||
const today0930 = now.hour(9).minute(30).second(0);
|
|
||||||
const today1130 = now.hour(11).minute(30).second(0);
|
|
||||||
const today1300 = now.hour(13).minute(0).second(0);
|
|
||||||
const today1500 = now.hour(15).minute(0).second(0);
|
|
||||||
const todayStart = now.startOf('day');
|
|
||||||
const todayEnd = now.endOf('day');
|
|
||||||
|
|
||||||
// 昨日关键时间点
|
|
||||||
const yesterday1500 = now.subtract(1, 'day').hour(15).minute(0).second(0);
|
|
||||||
const yesterdayStart = now.subtract(1, 'day').startOf('day');
|
|
||||||
const yesterdayEnd = now.subtract(1, 'day').endOf('day');
|
|
||||||
|
|
||||||
// 动态按钮配置(根据时段返回不同按钮数组)
|
|
||||||
// 注意:"当前交易日"已在固定按钮中,这里只放特定时段的快捷按钮
|
|
||||||
const dynamicButtonsMap = {
|
|
||||||
'pre-market': [], // 盘前:使用"当前交易日"即可
|
|
||||||
'morning': [
|
|
||||||
{
|
|
||||||
key: 'intraday',
|
|
||||||
label: '盘中',
|
|
||||||
range: [today0930, today1500],
|
|
||||||
tooltip: '盘中交易时段',
|
|
||||||
timeHint: '今日 09:30 - 15:00',
|
|
||||||
color: 'blue',
|
|
||||||
type: 'precise'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'lunch': [], // 午休:使用"当前交易日"即可
|
|
||||||
'afternoon': [
|
|
||||||
{
|
|
||||||
key: 'intraday',
|
|
||||||
label: '盘中',
|
|
||||||
range: [today0930, today1500],
|
|
||||||
tooltip: '盘中交易时段',
|
|
||||||
timeHint: '今日 09:30 - 15:00',
|
|
||||||
color: 'blue',
|
|
||||||
type: 'precise'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'afternoon',
|
|
||||||
label: '午盘',
|
|
||||||
range: [today1300, today1500],
|
|
||||||
tooltip: '午盘交易时段',
|
|
||||||
timeHint: '今日 13:00 - 15:00',
|
|
||||||
color: 'cyan',
|
|
||||||
type: 'precise'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'after-hours': [] // 盘后:使用"当前交易日"即可
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取上一个交易日(使用 tdays.csv 数据)
|
|
||||||
const getPrevTradingDay = () => {
|
|
||||||
try {
|
|
||||||
const prevTradingDay = tradingDayUtils.getPreviousTradingDay(now.toDate());
|
|
||||||
return dayjs(prevTradingDay);
|
|
||||||
} catch (e) {
|
|
||||||
// 降级:简单地减一天(不考虑周末节假日)
|
|
||||||
logger.warn('TradingTimeFilter', '获取上一交易日失败,降级处理', e);
|
|
||||||
return now.subtract(1, 'day');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const prevTradingDay = getPrevTradingDay();
|
|
||||||
const prevTradingDay1500 = prevTradingDay.hour(15).minute(0).second(0);
|
|
||||||
|
|
||||||
// 固定按钮配置(始终显示)
|
|
||||||
const fixedButtons = [
|
|
||||||
{
|
|
||||||
key: 'current-trading-day',
|
|
||||||
label: '当前交易日',
|
|
||||||
range: [prevTradingDay1500, now],
|
|
||||||
tooltip: '当前交易日事件',
|
|
||||||
timeHint: `${prevTradingDay.format('MM-DD')} 15:00 - 现在`,
|
|
||||||
color: 'green',
|
|
||||||
type: 'precise'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'morning-fixed',
|
|
||||||
label: '早盘',
|
|
||||||
range: [today0930, today1130],
|
|
||||||
tooltip: '早盘交易时段',
|
|
||||||
timeHint: '09:30 - 11:30',
|
|
||||||
color: 'geekblue',
|
|
||||||
type: 'precise'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'today',
|
|
||||||
label: '今日全天',
|
|
||||||
range: [todayStart, todayEnd],
|
|
||||||
tooltip: '今日全天',
|
|
||||||
timeHint: '今日 00:00 - 23:59',
|
|
||||||
color: 'purple',
|
|
||||||
type: 'precise'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'yesterday',
|
|
||||||
label: '昨日',
|
|
||||||
range: [yesterdayStart, yesterdayEnd],
|
|
||||||
tooltip: '昨日全天',
|
|
||||||
timeHint: '昨日 00:00 - 23:59',
|
|
||||||
color: 'orange',
|
|
||||||
type: 'precise'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'week',
|
|
||||||
label: '近一周',
|
|
||||||
range: 7, // 天数
|
|
||||||
tooltip: '过去7个交易日',
|
|
||||||
timeHint: '过去7天',
|
|
||||||
color: 'magenta',
|
|
||||||
type: 'recent_days'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'month',
|
|
||||||
label: '近一月',
|
|
||||||
range: 30, // 天数
|
|
||||||
tooltip: '过去30个交易日',
|
|
||||||
timeHint: '过去30天',
|
|
||||||
color: 'volcano',
|
|
||||||
type: 'recent_days'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'all',
|
|
||||||
label: '全部',
|
|
||||||
range: null, // 无时间限制
|
|
||||||
tooltip: '显示全部事件',
|
|
||||||
timeHint: '不限时间',
|
|
||||||
color: 'default',
|
|
||||||
type: 'all'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
|
||||||
dynamic: dynamicButtonsMap[session] || [],
|
|
||||||
fixed: fixedButtons
|
|
||||||
};
|
|
||||||
}, []); // 空依赖,首次渲染时计算
|
|
||||||
|
|
||||||
// 按钮点击处理
|
// 按钮点击处理
|
||||||
const handleButtonClick = (config) => {
|
const handleButtonClick = (config) => {
|
||||||
logger.debug('TradingTimeFilter', '按钮点击', {
|
logger.debug("TradingTimeFilter", "按钮点击", {
|
||||||
config,
|
config,
|
||||||
currentSelectedKey: selectedKey,
|
currentSelectedKey: selectedKey,
|
||||||
willToggle: selectedKey === config.key
|
willToggle: selectedKey === config.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (selectedKey === config.key) {
|
if (selectedKey === config.key) {
|
||||||
// 取消选中
|
|
||||||
setSelectedKey(null);
|
setSelectedKey(null);
|
||||||
onChange(null);
|
onChange(null);
|
||||||
logger.debug('TradingTimeFilter', '取消选中', { key: config.key });
|
logger.debug("TradingTimeFilter", "取消选中", { key: config.key });
|
||||||
} else {
|
} else {
|
||||||
// 选中
|
|
||||||
setSelectedKey(config.key);
|
setSelectedKey(config.key);
|
||||||
const timeConfig = {
|
onChange({
|
||||||
range: config.range,
|
range: config.range,
|
||||||
type: config.type,
|
type: config.type,
|
||||||
label: config.label,
|
label: config.label,
|
||||||
key: config.key
|
key: config.key,
|
||||||
};
|
});
|
||||||
onChange(timeConfig);
|
logger.debug("TradingTimeFilter", "选中新按钮", { key: config.key });
|
||||||
logger.debug('TradingTimeFilter', '选中新按钮', { timeConfig });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -241,27 +86,29 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
|||||||
const handleCustomRangeOk = (dates) => {
|
const handleCustomRangeOk = (dates) => {
|
||||||
if (dates && dates.length === 2) {
|
if (dates && dates.length === 2) {
|
||||||
setCustomRange(dates);
|
setCustomRange(dates);
|
||||||
setSelectedKey('custom');
|
setSelectedKey("custom");
|
||||||
setCustomRangeVisible(false);
|
setCustomRangeVisible(false);
|
||||||
|
|
||||||
onChange({
|
onChange({
|
||||||
range: dates,
|
range: dates,
|
||||||
type: 'precise',
|
type: "precise",
|
||||||
label: `${dates[0].format('MM-DD HH:mm')} - ${dates[1].format('MM-DD HH:mm')}`,
|
label: `${dates[0].format("MM-DD HH:mm")} - ${dates[1].format("MM-DD HH:mm")}`,
|
||||||
key: 'custom'
|
key: "custom",
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug('TradingTimeFilter', '自定义范围', {
|
logger.debug("TradingTimeFilter", "自定义范围", {
|
||||||
start: dates[0].format('YYYY-MM-DD HH:mm:ss'),
|
start: dates[0].format("YYYY-MM-DD HH:mm:ss"),
|
||||||
end: dates[1].format('YYYY-MM-DD HH:mm:ss')
|
end: dates[1].format("YYYY-MM-DD HH:mm:ss"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染紧凑模式按钮(PC 端搜索栏内使用,文字按钮 + | 分隔符)
|
// 渲染紧凑模式按钮
|
||||||
const renderCompactButton = (config, showDivider = true) => {
|
const renderCompactButton = (config, showDivider = true) => {
|
||||||
const isSelected = selectedKey === config.key;
|
const isSelected = selectedKey === config.key;
|
||||||
const fullTooltip = config.timeHint ? `${config.tooltip} · ${config.timeHint}` : config.tooltip;
|
const fullTooltip = config.timeHint
|
||||||
|
? `${config.tooltip} · ${config.timeHint}`
|
||||||
|
: config.tooltip;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={config.key}>
|
<React.Fragment key={config.key}>
|
||||||
@@ -269,41 +116,51 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
|||||||
<span
|
<span
|
||||||
onClick={() => handleButtonClick(config)}
|
onClick={() => handleButtonClick(config)}
|
||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: "pointer",
|
||||||
padding: '4px 8px',
|
padding: "4px 8px",
|
||||||
borderRadius: '4px',
|
borderRadius: "4px",
|
||||||
fontSize: '13px',
|
fontSize: "13px",
|
||||||
fontWeight: isSelected ? 600 : 400,
|
fontWeight: isSelected ? 600 : 400,
|
||||||
color: isSelected ? PROFESSIONAL_COLORS.gold[500] : PROFESSIONAL_COLORS.text.secondary,
|
color: isSelected
|
||||||
background: isSelected ? 'rgba(255, 195, 0, 0.15)' : 'transparent',
|
? PROFESSIONAL_COLORS.gold[500]
|
||||||
transition: 'all 0.2s ease',
|
: PROFESSIONAL_COLORS.text.secondary,
|
||||||
whiteSpace: 'nowrap',
|
background: isSelected
|
||||||
|
? "rgba(255, 195, 0, 0.15)"
|
||||||
|
: "transparent",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{config.label}
|
{config.label}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{showDivider && (
|
{showDivider && (
|
||||||
<span style={{ color: 'rgba(255, 255, 255, 0.2)', margin: '0 2px' }}>|</span>
|
<span style={{ color: "rgba(255, 255, 255, 0.2)", margin: "0 2px" }}>
|
||||||
|
|
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染按钮(默认模式)
|
// 渲染默认按钮
|
||||||
const renderButton = (config) => {
|
const renderButton = (config) => {
|
||||||
const isSelected = selectedKey === config.key;
|
const isSelected = selectedKey === config.key;
|
||||||
|
const fullTooltip = config.timeHint
|
||||||
// 构建完整的 tooltip 提示(文字 + 时间)
|
? `${config.tooltip} · ${config.timeHint}`
|
||||||
const fullTooltip = config.timeHint ? `${config.tooltip} · ${config.timeHint}` : config.tooltip;
|
: config.tooltip;
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
// 选中状态:只显示 Tag,不显示下方时间
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={fullTooltip} key={config.key}>
|
<Tooltip title={fullTooltip} key={config.key}>
|
||||||
<Tag
|
<Tag
|
||||||
color={config.color}
|
color={config.color}
|
||||||
style={{ margin: 0, fontSize: 13, padding: '2px 8px', cursor: 'pointer' }}
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 13,
|
||||||
|
padding: "2px 8px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
onClick={() => handleButtonClick(config)}
|
onClick={() => handleButtonClick(config)}
|
||||||
>
|
>
|
||||||
<ClockCircleOutlined style={{ marginRight: 4 }} />
|
<ClockCircleOutlined style={{ marginRight: 4 }} />
|
||||||
@@ -312,7 +169,6 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// 未选中状态:普通按钮
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={fullTooltip} key={config.key}>
|
<Tooltip title={fullTooltip} key={config.key}>
|
||||||
<Button
|
<Button
|
||||||
@@ -327,75 +183,36 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 禁用未来日期
|
// 自定义时间选择器内容
|
||||||
const disabledDate = (current) => {
|
|
||||||
return current && current > dayjs().endOf('day');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 禁用未来时间(精确到分钟)
|
|
||||||
const disabledTime = (current) => {
|
|
||||||
if (!current) return {};
|
|
||||||
|
|
||||||
const now = dayjs();
|
|
||||||
const isToday = current.isSame(now, 'day');
|
|
||||||
|
|
||||||
if (!isToday) return {};
|
|
||||||
|
|
||||||
const currentHour = now.hour();
|
|
||||||
const currentMinute = now.minute();
|
|
||||||
|
|
||||||
return {
|
|
||||||
disabledHours: () => {
|
|
||||||
const hours = [];
|
|
||||||
for (let i = currentHour + 1; i < 24; i++) {
|
|
||||||
hours.push(i);
|
|
||||||
}
|
|
||||||
return hours;
|
|
||||||
},
|
|
||||||
disabledMinutes: (selectedHour) => {
|
|
||||||
if (selectedHour === currentHour) {
|
|
||||||
const minutes = [];
|
|
||||||
for (let i = currentMinute + 1; i < 60; i++) {
|
|
||||||
minutes.push(i);
|
|
||||||
}
|
|
||||||
return minutes;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// "更多时间" 按钮内容
|
|
||||||
const customRangeContent = (
|
const customRangeContent = (
|
||||||
<div style={{ padding: 8 }}>
|
<div style={{ padding: 8 }}>
|
||||||
<RangePicker
|
<RangePicker
|
||||||
showTime={{ format: 'HH:mm' }}
|
showTime={{ format: "HH:mm" }}
|
||||||
format="YYYY-MM-DD HH:mm"
|
format="YYYY-MM-DD HH:mm"
|
||||||
locale={locale}
|
locale={locale}
|
||||||
placeholder={['开始时间', '结束时间']}
|
placeholder={["开始时间", "结束时间"]}
|
||||||
onChange={handleCustomRangeOk}
|
onChange={handleCustomRangeOk}
|
||||||
value={customRange}
|
value={customRange}
|
||||||
disabledDate={disabledDate}
|
disabledDate={disabledDate}
|
||||||
disabledTime={disabledTime}
|
disabledTime={disabledTime}
|
||||||
style={{ marginBottom: 8 }}
|
style={{ marginBottom: 8 }}
|
||||||
/>
|
/>
|
||||||
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>
|
<div style={{ fontSize: 12, color: "#999", marginTop: 4 }}>
|
||||||
支持精确到分钟的时间范围选择(不能超过当前时间)
|
支持精确到分钟的时间范围选择(不能超过当前时间)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 移动端模式:下拉选择器
|
// 移动端模式
|
||||||
if (mobile) {
|
if (mobile) {
|
||||||
const allButtons = [...timeRangeConfig.dynamic, ...timeRangeConfig.fixed];
|
const allButtons = [...timeRangeConfig.dynamic, ...timeRangeConfig.fixed];
|
||||||
|
|
||||||
const handleMobileSelect = (key) => {
|
const handleMobileSelect = (key) => {
|
||||||
if (key === selectedKey) {
|
if (key === selectedKey) {
|
||||||
// 取消选中
|
|
||||||
setSelectedKey(null);
|
setSelectedKey(null);
|
||||||
onChange(null);
|
onChange(null);
|
||||||
} else {
|
} else {
|
||||||
const config = allButtons.find(b => b.key === key);
|
const config = allButtons.find((b) => b.key === key);
|
||||||
if (config) {
|
if (config) {
|
||||||
handleButtonClick(config);
|
handleButtonClick(config);
|
||||||
}
|
}
|
||||||
@@ -407,7 +224,7 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
|||||||
value={selectedKey}
|
value={selectedKey}
|
||||||
onChange={handleMobileSelect}
|
onChange={handleMobileSelect}
|
||||||
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 }} />
|
||||||
筛选
|
筛选
|
||||||
</span>
|
</span>
|
||||||
@@ -421,7 +238,7 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
|||||||
className="transparent-select"
|
className="transparent-select"
|
||||||
popupMatchSelectWidth={false}
|
popupMatchSelectWidth={false}
|
||||||
>
|
>
|
||||||
{allButtons.map(config => (
|
{allButtons.map((config) => (
|
||||||
<Option key={config.key} value={config.key}>
|
<Option key={config.key} value={config.key}>
|
||||||
{config.label}
|
{config.label}
|
||||||
</Option>
|
</Option>
|
||||||
@@ -430,18 +247,20 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 紧凑模式:PC 端搜索栏内的样式
|
// 紧凑模式
|
||||||
if (compact) {
|
if (compact) {
|
||||||
// 合并所有按钮配置
|
|
||||||
const allButtons = [...timeRangeConfig.dynamic, ...timeRangeConfig.fixed];
|
const allButtons = [...timeRangeConfig.dynamic, ...timeRangeConfig.fixed];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'nowrap' }}>
|
<div
|
||||||
|
style={{ display: "flex", alignItems: "center", flexWrap: "nowrap" }}
|
||||||
|
>
|
||||||
{allButtons.map((config, index) =>
|
{allButtons.map((config, index) =>
|
||||||
renderCompactButton(config, index < allButtons.length - 1)
|
renderCompactButton(config, index < allButtons.length - 1)
|
||||||
)}
|
)}
|
||||||
{/* 更多时间 */}
|
<span style={{ color: "rgba(255, 255, 255, 0.2)", margin: "0 2px" }}>
|
||||||
<span style={{ color: 'rgba(255, 255, 255, 0.2)', margin: '0 2px' }}>|</span>
|
|
|
||||||
|
</span>
|
||||||
<Popover
|
<Popover
|
||||||
content={customRangeContent}
|
content={customRangeContent}
|
||||||
title="选择自定义时间范围"
|
title="选择自定义时间范围"
|
||||||
@@ -453,18 +272,24 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
|||||||
<Tooltip title="自定义时间范围">
|
<Tooltip title="自定义时间范围">
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: "pointer",
|
||||||
padding: '4px 8px',
|
padding: "4px 8px",
|
||||||
borderRadius: '4px',
|
borderRadius: "4px",
|
||||||
fontSize: '13px',
|
fontSize: "13px",
|
||||||
fontWeight: selectedKey === 'custom' ? 600 : 400,
|
fontWeight: selectedKey === "custom" ? 600 : 400,
|
||||||
color: selectedKey === 'custom' ? PROFESSIONAL_COLORS.gold[500] : PROFESSIONAL_COLORS.text.secondary,
|
color:
|
||||||
background: selectedKey === 'custom' ? 'rgba(255, 195, 0, 0.15)' : 'transparent',
|
selectedKey === "custom"
|
||||||
transition: 'all 0.2s ease',
|
? PROFESSIONAL_COLORS.gold[500]
|
||||||
whiteSpace: 'nowrap',
|
: PROFESSIONAL_COLORS.text.secondary,
|
||||||
display: 'flex',
|
background:
|
||||||
alignItems: 'center',
|
selectedKey === "custom"
|
||||||
gap: '4px',
|
? "rgba(255, 195, 0, 0.15)"
|
||||||
|
: "transparent",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "4px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CalendarOutlined style={{ fontSize: 12 }} />
|
<CalendarOutlined style={{ fontSize: 12 }} />
|
||||||
@@ -476,16 +301,16 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认模式:移动端/独立使用
|
// 默认模式
|
||||||
return (
|
return (
|
||||||
<Space wrap size={[8, 8]} style={{ display: 'flex', alignItems: 'flex-start' }}>
|
<Space
|
||||||
{/* 动态按钮(根据时段显示多个) */}
|
wrap
|
||||||
{timeRangeConfig.dynamic.map(config => renderButton(config))}
|
size={[8, 8]}
|
||||||
|
style={{ display: "flex", alignItems: "flex-start" }}
|
||||||
|
>
|
||||||
|
{timeRangeConfig.dynamic.map((config) => renderButton(config))}
|
||||||
|
{timeRangeConfig.fixed.map((config) => renderButton(config))}
|
||||||
|
|
||||||
{/* 固定按钮(始终显示) */}
|
|
||||||
{timeRangeConfig.fixed.map(config => renderButton(config))}
|
|
||||||
|
|
||||||
{/* 更多时间 - 自定义范围 */}
|
|
||||||
<Popover
|
<Popover
|
||||||
content={customRangeContent}
|
content={customRangeContent}
|
||||||
title="选择自定义时间范围"
|
title="选择自定义时间范围"
|
||||||
@@ -494,18 +319,36 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
|||||||
onOpenChange={setCustomRangeVisible}
|
onOpenChange={setCustomRangeVisible}
|
||||||
placement="bottomLeft"
|
placement="bottomLeft"
|
||||||
>
|
>
|
||||||
{selectedKey === 'custom' ? (
|
{selectedKey === "custom" ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={customRange ? `自定义时间范围 · ${customRange[0].format('MM-DD HH:mm')} - ${customRange[1].format('MM-DD HH:mm')}` : '自定义时间范围'}
|
title={
|
||||||
|
customRange
|
||||||
|
? `自定义时间范围 · ${customRange[0].format("MM-DD HH:mm")} - ${customRange[1].format("MM-DD HH:mm")}`
|
||||||
|
: "自定义时间范围"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Tag color="gold" style={{ margin: 0, fontSize: 13, padding: '2px 8px', cursor: 'pointer' }}>
|
<Tag
|
||||||
|
color="gold"
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 13,
|
||||||
|
padding: "2px 8px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CalendarOutlined style={{ marginRight: 4 }} />
|
<CalendarOutlined style={{ marginRight: 4 }} />
|
||||||
{customRange ? `${customRange[0].format('MM-DD HH:mm')} - ${customRange[1].format('MM-DD HH:mm')}` : '自定义'}
|
{customRange
|
||||||
|
? `${customRange[0].format("MM-DD HH:mm")} - ${customRange[1].format("MM-DD HH:mm")}`
|
||||||
|
: "自定义"}
|
||||||
</Tag>
|
</Tag>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip title="自定义时间范围">
|
<Tooltip title="自定义时间范围">
|
||||||
<Button size="small" icon={<CalendarOutlined />} style={{ fontSize: 12 }}>
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<CalendarOutlined />}
|
||||||
|
style={{ fontSize: 12 }}
|
||||||
|
>
|
||||||
更多时间
|
更多时间
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// TradingTimeFilter 常量定义
|
||||||
|
|
||||||
|
// 时段边界(分钟数)
|
||||||
|
export const TIME_BOUNDARIES = {
|
||||||
|
PRE_MARKET_END: 9 * 60 + 30, // 09:30
|
||||||
|
MORNING_END: 11 * 60 + 30, // 11:30
|
||||||
|
AFTERNOON_START: 13 * 60, // 13:00
|
||||||
|
MARKET_CLOSE: 15 * 60, // 15:00
|
||||||
|
};
|
||||||
|
|
||||||
|
// 时段类型
|
||||||
|
export const TRADING_SESSIONS = {
|
||||||
|
PRE_MARKET: "pre-market", // 盘前
|
||||||
|
MORNING: "morning", // 早盘
|
||||||
|
LUNCH: "lunch", // 午休
|
||||||
|
AFTERNOON: "afternoon", // 午盘
|
||||||
|
AFTER_HOURS: "after-hours", // 盘后
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// TradingTimeFilter 模块导出
|
||||||
|
|
||||||
|
export { TIME_BOUNDARIES, TRADING_SESSIONS } from "./constants";
|
||||||
|
export {
|
||||||
|
getCurrentTradingSession,
|
||||||
|
getPrevTradingDay,
|
||||||
|
generateTimeRangeConfig,
|
||||||
|
disabledDate,
|
||||||
|
disabledTime,
|
||||||
|
} from "./utils";
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
// TradingTimeFilter 工具函数
|
||||||
|
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { TIME_BOUNDARIES, TRADING_SESSIONS } from "./constants";
|
||||||
|
import { logger } from "@utils/logger";
|
||||||
|
import tradingDayUtils from "@utils/tradingDayUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前交易时段
|
||||||
|
* @returns {string} 时段标识
|
||||||
|
*/
|
||||||
|
export const getCurrentTradingSession = () => {
|
||||||
|
const now = dayjs();
|
||||||
|
const hour = now.hour();
|
||||||
|
const minute = now.minute();
|
||||||
|
const currentMinutes = hour * 60 + minute;
|
||||||
|
|
||||||
|
if (currentMinutes < TIME_BOUNDARIES.PRE_MARKET_END) {
|
||||||
|
return TRADING_SESSIONS.PRE_MARKET;
|
||||||
|
} else if (currentMinutes < TIME_BOUNDARIES.MORNING_END) {
|
||||||
|
return TRADING_SESSIONS.MORNING;
|
||||||
|
} else if (currentMinutes < TIME_BOUNDARIES.AFTERNOON_START) {
|
||||||
|
return TRADING_SESSIONS.LUNCH;
|
||||||
|
} else if (currentMinutes < TIME_BOUNDARIES.MARKET_CLOSE) {
|
||||||
|
return TRADING_SESSIONS.AFTERNOON;
|
||||||
|
} else {
|
||||||
|
return TRADING_SESSIONS.AFTER_HOURS;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取上一个交易日
|
||||||
|
* @param {dayjs.Dayjs} now - 当前时间
|
||||||
|
* @returns {dayjs.Dayjs} 上一交易日
|
||||||
|
*/
|
||||||
|
export const getPrevTradingDay = (now) => {
|
||||||
|
try {
|
||||||
|
const prevTradingDay = tradingDayUtils.getPreviousTradingDay(now.toDate());
|
||||||
|
return dayjs(prevTradingDay);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn("TradingTimeFilter", "获取上一交易日失败,降级处理", e);
|
||||||
|
return now.subtract(1, "day");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成时间范围配置
|
||||||
|
* @returns {Object} 包含 dynamic 和 fixed 按钮配置
|
||||||
|
*/
|
||||||
|
export const generateTimeRangeConfig = () => {
|
||||||
|
const session = getCurrentTradingSession();
|
||||||
|
const now = dayjs();
|
||||||
|
|
||||||
|
// 今日关键时间点
|
||||||
|
const today0930 = now.hour(9).minute(30).second(0);
|
||||||
|
const today1130 = now.hour(11).minute(30).second(0);
|
||||||
|
const today1300 = now.hour(13).minute(0).second(0);
|
||||||
|
const today1500 = now.hour(15).minute(0).second(0);
|
||||||
|
const todayStart = now.startOf("day");
|
||||||
|
const todayEnd = now.endOf("day");
|
||||||
|
|
||||||
|
// 昨日关键时间点
|
||||||
|
const yesterdayStart = now.subtract(1, "day").startOf("day");
|
||||||
|
const yesterdayEnd = now.subtract(1, "day").endOf("day");
|
||||||
|
|
||||||
|
// 动态按钮配置(根据时段)
|
||||||
|
const dynamicButtonsMap = {
|
||||||
|
[TRADING_SESSIONS.PRE_MARKET]: [],
|
||||||
|
[TRADING_SESSIONS.MORNING]: [
|
||||||
|
{
|
||||||
|
key: "intraday",
|
||||||
|
label: "盘中",
|
||||||
|
range: [today0930, today1500],
|
||||||
|
tooltip: "盘中交易时段",
|
||||||
|
timeHint: "今日 09:30 - 15:00",
|
||||||
|
color: "blue",
|
||||||
|
type: "precise",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[TRADING_SESSIONS.LUNCH]: [],
|
||||||
|
[TRADING_SESSIONS.AFTERNOON]: [
|
||||||
|
{
|
||||||
|
key: "intraday",
|
||||||
|
label: "盘中",
|
||||||
|
range: [today0930, today1500],
|
||||||
|
tooltip: "盘中交易时段",
|
||||||
|
timeHint: "今日 09:30 - 15:00",
|
||||||
|
color: "blue",
|
||||||
|
type: "precise",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "afternoon",
|
||||||
|
label: "午盘",
|
||||||
|
range: [today1300, today1500],
|
||||||
|
tooltip: "午盘交易时段",
|
||||||
|
timeHint: "今日 13:00 - 15:00",
|
||||||
|
color: "cyan",
|
||||||
|
type: "precise",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[TRADING_SESSIONS.AFTER_HOURS]: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevTradingDay = getPrevTradingDay(now);
|
||||||
|
const prevTradingDay1500 = prevTradingDay.hour(15).minute(0).second(0);
|
||||||
|
|
||||||
|
// 固定按钮配置
|
||||||
|
const fixedButtons = [
|
||||||
|
{
|
||||||
|
key: "current-trading-day",
|
||||||
|
label: "当前交易日",
|
||||||
|
range: [prevTradingDay1500, now],
|
||||||
|
tooltip: "当前交易日事件",
|
||||||
|
timeHint: `${prevTradingDay.format("MM-DD")} 15:00 - 现在`,
|
||||||
|
color: "green",
|
||||||
|
type: "precise",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "morning-fixed",
|
||||||
|
label: "早盘",
|
||||||
|
range: [today0930, today1130],
|
||||||
|
tooltip: "早盘交易时段",
|
||||||
|
timeHint: "09:30 - 11:30",
|
||||||
|
color: "geekblue",
|
||||||
|
type: "precise",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "today",
|
||||||
|
label: "今日全天",
|
||||||
|
range: [todayStart, todayEnd],
|
||||||
|
tooltip: "今日全天",
|
||||||
|
timeHint: "今日 00:00 - 23:59",
|
||||||
|
color: "purple",
|
||||||
|
type: "precise",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "yesterday",
|
||||||
|
label: "昨日",
|
||||||
|
range: [yesterdayStart, yesterdayEnd],
|
||||||
|
tooltip: "昨日全天",
|
||||||
|
timeHint: "昨日 00:00 - 23:59",
|
||||||
|
color: "orange",
|
||||||
|
type: "precise",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "week",
|
||||||
|
label: "近一周",
|
||||||
|
range: 7,
|
||||||
|
tooltip: "过去7个交易日",
|
||||||
|
timeHint: "过去7天",
|
||||||
|
color: "magenta",
|
||||||
|
type: "recent_days",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "month",
|
||||||
|
label: "近一月",
|
||||||
|
range: 30,
|
||||||
|
tooltip: "过去30个交易日",
|
||||||
|
timeHint: "过去30天",
|
||||||
|
color: "volcano",
|
||||||
|
type: "recent_days",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "all",
|
||||||
|
label: "全部",
|
||||||
|
range: null,
|
||||||
|
tooltip: "显示全部事件",
|
||||||
|
timeHint: "不限时间",
|
||||||
|
color: "default",
|
||||||
|
type: "all",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
dynamic: dynamicButtonsMap[session] || [],
|
||||||
|
fixed: fixedButtons,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用未来日期
|
||||||
|
* @param {dayjs.Dayjs} current - 当前日期
|
||||||
|
* @returns {boolean} 是否禁用
|
||||||
|
*/
|
||||||
|
export const disabledDate = (current) => {
|
||||||
|
return current && current > dayjs().endOf("day");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用未来时间(精确到分钟)
|
||||||
|
* @param {dayjs.Dayjs} current - 当前时间
|
||||||
|
* @returns {Object} 禁用配置
|
||||||
|
*/
|
||||||
|
export const disabledTime = (current) => {
|
||||||
|
if (!current) return {};
|
||||||
|
|
||||||
|
const now = dayjs();
|
||||||
|
const isToday = current.isSame(now, "day");
|
||||||
|
|
||||||
|
if (!isToday) return {};
|
||||||
|
|
||||||
|
const currentHour = now.hour();
|
||||||
|
const currentMinute = now.minute();
|
||||||
|
|
||||||
|
return {
|
||||||
|
disabledHours: () => {
|
||||||
|
const hours = [];
|
||||||
|
for (let i = currentHour + 1; i < 24; i++) {
|
||||||
|
hours.push(i);
|
||||||
|
}
|
||||||
|
return hours;
|
||||||
|
},
|
||||||
|
disabledMinutes: (selectedHour) => {
|
||||||
|
if (selectedHour === currentHour) {
|
||||||
|
const minutes = [];
|
||||||
|
for (let i = currentMinute + 1; i < 60; i++) {
|
||||||
|
minutes.push(i);
|
||||||
|
}
|
||||||
|
return minutes;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user