refactor(TradeDatePicker): 组件重构,配置提取与性能优化

- 拆分文件:types.ts(类型)、theme.ts(主题)、utils.ts(工具函数)
- 移除 isDarkMode 相关代码(已确认仅浅色模式)
- 移除 useColorModeValue,直接使用固定颜色值
- 子组件使用 memo 优化,主组件使用 useCallback/useMemo
- 清理冗余:移除未使用的 tipIcon、重复的 focus 样式
- 更新调用方移除 isDarkMode prop

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-31 13:23:28 +08:00
parent 70fdad9751
commit 927668bb9c
6 changed files with 180 additions and 129 deletions

View File

@@ -1,45 +1,55 @@
import React from 'react';
import {
HStack,
Input,
Text,
Icon,
Tooltip,
useColorModeValue,
} from '@chakra-ui/react';
/**
* TradeDatePicker 交易日期选择器组件
*/
import React, { memo, useCallback, useMemo, useEffect } from 'react';
import { HStack, Input, Text, Icon, Tooltip } from '@chakra-ui/react';
import { Info, Calendar } from 'lucide-react';
export interface TradeDatePickerProps {
/** 当前选中的日期 */
value: Date | null;
/** 日期变化回调 */
onChange: (date: Date) => void;
/** 默认日期(组件初始化时使用) */
defaultDate?: Date;
/** 最新交易日期(用于显示提示) */
latestTradeDate?: Date | null;
/** 最小可选日期 */
minDate?: Date;
/** 最大可选日期,默认今天 */
maxDate?: Date;
/** 标签文字,默认"交易日期" */
label?: string;
/** 输入框宽度 */
inputWidth?: string | object;
/** 是否显示标签图标 */
showIcon?: boolean;
/** 是否使用深色模式(强制覆盖 Chakra 颜色模式) */
isDarkMode?: boolean;
/** 是否显示最新交易日期提示,默认 true */
showLatestTradeDateTip?: boolean;
}
import type { TradeDatePickerProps } from './types';
import { COLORS, INPUT_HOVER_STYLE, SIZE_CONFIG } from './theme';
import { formatDateToString, formatDateToChinese, getTodayString } from './utils';
/**
* 交易日期选择器组件
*
* 提供日期输入框和最新交易日期提示,供概念中心、个股中心等页面复用。
* 快捷按钮(今天、昨天等)由各页面自行实现。
*/
export type { TradeDatePickerProps } from './types';
/** 日期标签 */
const DateLabel = memo<{
label: string;
showIcon: boolean;
iconSize: number;
spacing: number;
}>(({ label, showIcon, iconSize, spacing }) => (
<HStack spacing={spacing}>
{showIcon && <Icon as={Calendar} color={COLORS.icon} boxSize={iconSize} />}
<Text fontWeight="bold" color={COLORS.label}>
{label}
</Text>
</HStack>
));
DateLabel.displayName = 'DateLabel';
/** 最新交易日期提示 */
const LatestDateTip = memo<{ date: Date }>(({ date }) => (
<Tooltip label="数据库中最新的交易日期">
<HStack
spacing={1.5}
ml="auto"
px={2}
py={1}
opacity={0.7}
_hover={{ opacity: 1 }}
transition="opacity 0.2s"
>
<Info size={12} color="var(--chakra-colors-blue-300)" style={{ display: 'inline-block' }} />
<Text fontSize="xs" color={COLORS.tipText}>
{formatDateToChinese(date)}
</Text>
</HStack>
</Tooltip>
));
LatestDateTip.displayName = 'LatestDateTip';
/** 主组件 */
const TradeDatePicker: React.FC<TradeDatePickerProps> = ({
value,
onChange,
@@ -50,121 +60,69 @@ const TradeDatePicker: React.FC<TradeDatePickerProps> = ({
label = '交易日期',
inputWidth = { base: '100%', lg: '200px' },
showIcon = true,
isDarkMode = false,
showLatestTradeDateTip = true,
size = 'md',
}) => {
// 颜色主题 - 支持 isDarkMode 强制覆盖
const defaultLabelColor = useColorModeValue('purple.700', 'purple.300');
const defaultIconColor = useColorModeValue('purple.500', 'purple.400');
const defaultInputBorderColor = useColorModeValue('purple.200', 'purple.600');
const defaultTipBg = useColorModeValue('blue.50', 'blue.900');
const defaultTipBorderColor = useColorModeValue('blue.200', 'blue.600');
const defaultTipTextColor = useColorModeValue('blue.600', 'blue.200');
const defaultTipIconColor = useColorModeValue('blue.500', 'blue.300');
const sizeConfig = SIZE_CONFIG[size];
// 深色模式专用颜色
const darkModeColors = {
labelColor: 'white',
iconColor: 'cyan.400',
inputBorderColor: 'whiteAlpha.300',
inputBg: 'whiteAlpha.50',
inputColor: 'white',
tipBg: 'rgba(59, 130, 246, 0.15)',
tipBorderColor: 'blue.500',
tipTextColor: 'blue.200',
tipIconColor: 'blue.300',
};
// 根据 isDarkMode 选择颜色
const labelColor = isDarkMode ? darkModeColors.labelColor : defaultLabelColor;
const iconColor = isDarkMode ? darkModeColors.iconColor : defaultIconColor;
const inputBorderColor = isDarkMode ? darkModeColors.inputBorderColor : defaultInputBorderColor;
const tipBg = isDarkMode ? darkModeColors.tipBg : defaultTipBg;
const tipBorderColor = isDarkMode ? darkModeColors.tipBorderColor : defaultTipBorderColor;
const tipTextColor = isDarkMode ? darkModeColors.tipTextColor : defaultTipTextColor;
const tipIconColor = isDarkMode ? darkModeColors.tipIconColor : defaultTipIconColor;
// 使用默认日期初始化(仅在 value 为 null 且有 defaultDate 时)
React.useEffect(() => {
// 默认日期初始化
useEffect(() => {
if (value === null && defaultDate) {
onChange(defaultDate);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 处理日期变化
const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const dateStr = e.target.value;
if (dateStr) {
const date = new Date(dateStr);
onChange(date);
}
};
const handleDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.value) {
onChange(new Date(e.target.value));
}
},
[onChange]
);
// 格式化日期为 YYYY-MM-DD
const formatDateValue = (date: Date | null): string => {
if (!date) return '';
return date.toISOString().split('T')[0];
};
const { minDateStr, maxDateStr } = useMemo(
() => ({
minDateStr: minDate ? formatDateToString(minDate) : undefined,
maxDateStr: maxDate ? formatDateToString(maxDate) : getTodayString(),
}),
[minDate, maxDate]
);
// 计算日期范围
const minDateStr = minDate ? formatDateValue(minDate) : undefined;
const maxDateStr = maxDate
? formatDateValue(maxDate)
: new Date().toISOString().split('T')[0];
const valueStr = useMemo(() => formatDateToString(value), [value]);
return (
<>
{/* 标签 */}
<HStack spacing={3}>
{showIcon && <Icon as={Calendar} color={iconColor} boxSize={5} />}
<Text fontWeight="bold" color={labelColor}>
{label}
</Text>
</HStack>
<DateLabel
label={label}
showIcon={showIcon}
iconSize={sizeConfig.iconSize}
spacing={sizeConfig.spacing}
/>
{/* 日期输入框 */}
<Input
type="date"
value={formatDateValue(value)}
value={valueStr}
onChange={handleDateChange}
min={minDateStr}
max={maxDateStr}
width={inputWidth}
height={sizeConfig.inputHeight}
fontSize={sizeConfig.fontSize}
focusBorderColor="purple.400"
borderColor={inputBorderColor}
borderColor={COLORS.inputBorder}
borderRadius="lg"
fontWeight="medium"
bg={isDarkMode ? darkModeColors.inputBg : undefined}
color={isDarkMode ? darkModeColors.inputColor : undefined}
_hover={{ borderColor: isDarkMode ? 'purple.400' : 'purple.300' }}
sx={isDarkMode ? {
'&::-webkit-calendar-picker-indicator': {
filter: 'invert(1)',
},
} : undefined}
transition="all 0.2s"
_hover={INPUT_HOVER_STYLE}
/>
{/* 最新交易日期提示 - 靠右显示,样式更低调避免误认为按钮 */}
{showLatestTradeDateTip && latestTradeDate && (
<Tooltip label="数据库中最新的交易日期">
<HStack
spacing={1.5}
ml="auto"
px={2}
py={1}
opacity={0.7}
_hover={{ opacity: 1 }}
transition="opacity 0.2s"
>
<Info size={12} color="var(--chakra-colors-blue-300)" style={{ display: 'inline-block' }} />
<Text fontSize="xs" color={tipTextColor}>
{latestTradeDate.toLocaleDateString('zh-CN')}
</Text>
</HStack>
</Tooltip>
<LatestDateTip date={latestTradeDate} />
)}
</>
);
};
export default TradeDatePicker;
export default memo(TradeDatePicker);

View File

@@ -0,0 +1,30 @@
/**
* TradeDatePicker 主题配置
*/
/** 颜色配置 */
export const COLORS = {
label: 'purple.700',
icon: 'purple.500',
inputBorder: 'purple.200',
tipText: 'blue.600',
};
/** 输入框悬浮样式 */
export const INPUT_HOVER_STYLE = { borderColor: 'purple.300' };
/** 尺寸配置 */
export const SIZE_CONFIG = {
sm: {
inputHeight: '32px',
fontSize: 'sm',
iconSize: 4,
spacing: 2,
},
md: {
inputHeight: '40px',
fontSize: 'md',
iconSize: 5,
spacing: 3,
},
};

View File

@@ -0,0 +1,31 @@
/**
* TradeDatePicker 类型定义
*/
import type { ResponsiveValue } from '@chakra-ui/react';
/** 组件 Props */
export interface TradeDatePickerProps {
/** 当前选中的日期 */
value: Date | null;
/** 日期变化回调 */
onChange: (date: Date) => void;
/** 默认日期(组件初始化时使用) */
defaultDate?: Date;
/** 最新交易日期(用于显示提示) */
latestTradeDate?: Date | null;
/** 最小可选日期 */
minDate?: Date;
/** 最大可选日期,默认今天 */
maxDate?: Date;
/** 标签文字,默认"交易日期" */
label?: string;
/** 输入框宽度 */
inputWidth?: ResponsiveValue<string>;
/** 是否显示标签图标 */
showIcon?: boolean;
/** 是否显示最新交易日期提示,默认 true */
showLatestTradeDateTip?: boolean;
/** 尺寸sm | md */
size?: 'sm' | 'md';
}

View File

@@ -0,0 +1,34 @@
/**
* TradeDatePicker 工具函数
*/
/**
* 格式化日期为 YYYY-MM-DD 字符串
* @param date - 日期对象
* @returns 格式化后的日期字符串,如果 date 为 null 则返回空字符串
*/
export const formatDateToString = (date: Date | null): string => {
if (!date) return '';
// 使用本地时间避免时区问题
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
/**
* 格式化日期为中文本地化字符串
* @param date - 日期对象
* @returns 中文格式的日期字符串
*/
export const formatDateToChinese = (date: Date): string => {
return date.toLocaleDateString('zh-CN');
};
/**
* 获取今天的日期字符串 (YYYY-MM-DD)
* @returns 今天的日期字符串
*/
export const getTodayString = (): string => {
return formatDateToString(new Date());
};

View File

@@ -1467,7 +1467,6 @@ const ConceptCenter = () => {
}}
latestTradeDate={latestTradeDate}
label="交易日期"
isDarkMode={true}
showLatestTradeDateTip={false}
/>

View File

@@ -440,7 +440,6 @@ const HotspotOverview = ({ selectedDate, onDateChange, minDate, maxDate }) => {
latestTradeDate={null}
minDate={minDate}
maxDate={maxDate}
isDarkMode={true}
size="sm"
/>
)}