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
|
||||
// 交易时段智能筛选组件
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { Space, Button, Tag, Tooltip, DatePicker, Popover, Select } from 'antd';
|
||||
import { ClockCircleOutlined, CalendarOutlined, FilterOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import locale from 'antd/es/date-picker/locale/zh_CN';
|
||||
import { logger } from '@utils/logger';
|
||||
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme';
|
||||
import tradingDayUtils from '@utils/tradingDayUtils';
|
||||
|
||||
import React, { useState, useMemo, useEffect } from "react";
|
||||
import {
|
||||
Space,
|
||||
Button,
|
||||
Tag,
|
||||
Tooltip,
|
||||
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 { 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 [customRangeVisible, setCustomRangeVisible] = useState(false);
|
||||
const [customRange, setCustomRange] = useState(null);
|
||||
|
||||
// 监听外部 value 变化,同步内部状态
|
||||
// 监听外部 value 变化
|
||||
useEffect(() => {
|
||||
if (value === null || value === undefined) {
|
||||
// 外部重置,清空内部状态
|
||||
setSelectedKey(null);
|
||||
setCustomRange(null);
|
||||
logger.debug('TradingTimeFilter', '外部重置,清空选中状态');
|
||||
logger.debug("TradingTimeFilter", "外部重置,清空选中状态");
|
||||
} else {
|
||||
// 外部选中值变化,同步内部状态
|
||||
setSelectedKey(value);
|
||||
logger.debug('TradingTimeFilter', '外部value变化,同步内部状态', { value });
|
||||
logger.debug("TradingTimeFilter", "外部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 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 timeRangeConfig = useMemo(() => generateTimeRangeConfig(), []);
|
||||
|
||||
// 按钮点击处理
|
||||
const handleButtonClick = (config) => {
|
||||
logger.debug('TradingTimeFilter', '按钮点击', {
|
||||
logger.debug("TradingTimeFilter", "按钮点击", {
|
||||
config,
|
||||
currentSelectedKey: selectedKey,
|
||||
willToggle: selectedKey === config.key
|
||||
willToggle: selectedKey === config.key,
|
||||
});
|
||||
|
||||
if (selectedKey === config.key) {
|
||||
// 取消选中
|
||||
setSelectedKey(null);
|
||||
onChange(null);
|
||||
logger.debug('TradingTimeFilter', '取消选中', { key: config.key });
|
||||
logger.debug("TradingTimeFilter", "取消选中", { key: config.key });
|
||||
} else {
|
||||
// 选中
|
||||
setSelectedKey(config.key);
|
||||
const timeConfig = {
|
||||
onChange({
|
||||
range: config.range,
|
||||
type: config.type,
|
||||
label: config.label,
|
||||
key: config.key
|
||||
};
|
||||
onChange(timeConfig);
|
||||
logger.debug('TradingTimeFilter', '选中新按钮', { timeConfig });
|
||||
key: config.key,
|
||||
});
|
||||
logger.debug("TradingTimeFilter", "选中新按钮", { key: config.key });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -241,27 +86,29 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
||||
const handleCustomRangeOk = (dates) => {
|
||||
if (dates && dates.length === 2) {
|
||||
setCustomRange(dates);
|
||||
setSelectedKey('custom');
|
||||
setSelectedKey("custom");
|
||||
setCustomRangeVisible(false);
|
||||
|
||||
onChange({
|
||||
range: dates,
|
||||
type: 'precise',
|
||||
label: `${dates[0].format('MM-DD HH:mm')} - ${dates[1].format('MM-DD HH:mm')}`,
|
||||
key: 'custom'
|
||||
type: "precise",
|
||||
label: `${dates[0].format("MM-DD HH:mm")} - ${dates[1].format("MM-DD HH:mm")}`,
|
||||
key: "custom",
|
||||
});
|
||||
|
||||
logger.debug('TradingTimeFilter', '自定义范围', {
|
||||
start: dates[0].format('YYYY-MM-DD HH:mm:ss'),
|
||||
end: dates[1].format('YYYY-MM-DD HH:mm:ss')
|
||||
logger.debug("TradingTimeFilter", "自定义范围", {
|
||||
start: dates[0].format("YYYY-MM-DD HH:mm:ss"),
|
||||
end: dates[1].format("YYYY-MM-DD HH:mm:ss"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染紧凑模式按钮(PC 端搜索栏内使用,文字按钮 + | 分隔符)
|
||||
// 渲染紧凑模式按钮
|
||||
const renderCompactButton = (config, showDivider = true) => {
|
||||
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 (
|
||||
<React.Fragment key={config.key}>
|
||||
@@ -269,41 +116,51 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
||||
<span
|
||||
onClick={() => handleButtonClick(config)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '13px',
|
||||
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',
|
||||
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>
|
||||
<span style={{ color: "rgba(255, 255, 255, 0.2)", margin: "0 2px" }}>
|
||||
|
|
||||
</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染按钮(默认模式)
|
||||
// 渲染默认按钮
|
||||
const renderButton = (config) => {
|
||||
const isSelected = selectedKey === config.key;
|
||||
|
||||
// 构建完整的 tooltip 提示(文字 + 时间)
|
||||
const fullTooltip = config.timeHint ? `${config.tooltip} · ${config.timeHint}` : config.tooltip;
|
||||
const fullTooltip = config.timeHint
|
||||
? `${config.tooltip} · ${config.timeHint}`
|
||||
: config.tooltip;
|
||||
|
||||
if (isSelected) {
|
||||
// 选中状态:只显示 Tag,不显示下方时间
|
||||
return (
|
||||
<Tooltip title={fullTooltip} key={config.key}>
|
||||
<Tag
|
||||
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)}
|
||||
>
|
||||
<ClockCircleOutlined style={{ marginRight: 4 }} />
|
||||
@@ -312,7 +169,6 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
// 未选中状态:普通按钮
|
||||
return (
|
||||
<Tooltip title={fullTooltip} key={config.key}>
|
||||
<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 = (
|
||||
<div style={{ padding: 8 }}>
|
||||
<RangePicker
|
||||
showTime={{ format: 'HH:mm' }}
|
||||
showTime={{ format: "HH:mm" }}
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
locale={locale}
|
||||
placeholder={['开始时间', '结束时间']}
|
||||
placeholder={["开始时间", "结束时间"]}
|
||||
onChange={handleCustomRangeOk}
|
||||
value={customRange}
|
||||
disabledDate={disabledDate}
|
||||
disabledTime={disabledTime}
|
||||
style={{ marginBottom: 8 }}
|
||||
/>
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>
|
||||
<div style={{ fontSize: 12, color: "#999", marginTop: 4 }}>
|
||||
支持精确到分钟的时间范围选择(不能超过当前时间)
|
||||
</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);
|
||||
const config = allButtons.find((b) => b.key === key);
|
||||
if (config) {
|
||||
handleButtonClick(config);
|
||||
}
|
||||
@@ -407,7 +224,7 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
||||
value={selectedKey}
|
||||
onChange={handleMobileSelect}
|
||||
placeholder={
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 4 }}>
|
||||
<FilterOutlined style={{ fontSize: 12 }} />
|
||||
筛选
|
||||
</span>
|
||||
@@ -421,7 +238,7 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
||||
className="transparent-select"
|
||||
popupMatchSelectWidth={false}
|
||||
>
|
||||
{allButtons.map(config => (
|
||||
{allButtons.map((config) => (
|
||||
<Option key={config.key} value={config.key}>
|
||||
{config.label}
|
||||
</Option>
|
||||
@@ -430,18 +247,20 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
||||
);
|
||||
}
|
||||
|
||||
// 紧凑模式:PC 端搜索栏内的样式
|
||||
// 紧凑模式
|
||||
if (compact) {
|
||||
// 合并所有按钮配置
|
||||
const allButtons = [...timeRangeConfig.dynamic, ...timeRangeConfig.fixed];
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'nowrap' }}>
|
||||
<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>
|
||||
<span style={{ color: "rgba(255, 255, 255, 0.2)", margin: "0 2px" }}>
|
||||
|
|
||||
</span>
|
||||
<Popover
|
||||
content={customRangeContent}
|
||||
title="选择自定义时间范围"
|
||||
@@ -453,18 +272,24 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
||||
<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',
|
||||
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 }} />
|
||||
@@ -476,16 +301,16 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
||||
);
|
||||
}
|
||||
|
||||
// 默认模式:移动端/独立使用
|
||||
// 默认模式
|
||||
return (
|
||||
<Space wrap size={[8, 8]} style={{ display: 'flex', alignItems: 'flex-start' }}>
|
||||
{/* 动态按钮(根据时段显示多个) */}
|
||||
{timeRangeConfig.dynamic.map(config => renderButton(config))}
|
||||
<Space
|
||||
wrap
|
||||
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
|
||||
content={customRangeContent}
|
||||
title="选择自定义时间范围"
|
||||
@@ -494,18 +319,36 @@ const TradingTimeFilter = ({ value, onChange, compact = false, mobile = false })
|
||||
onOpenChange={setCustomRangeVisible}
|
||||
placement="bottomLeft"
|
||||
>
|
||||
{selectedKey === 'custom' ? (
|
||||
{selectedKey === "custom" ? (
|
||||
<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 }} />
|
||||
{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>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title="自定义时间范围">
|
||||
<Button size="small" icon={<CalendarOutlined />} style={{ fontSize: 12 }}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CalendarOutlined />}
|
||||
style={{ fontSize: 12 }}
|
||||
>
|
||||
更多时间
|
||||
</Button>
|
||||
</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