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:
@@ -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);
|
||||
|
||||
30
src/components/TradeDatePicker/theme.ts
Normal file
30
src/components/TradeDatePicker/theme.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
31
src/components/TradeDatePicker/types.ts
Normal file
31
src/components/TradeDatePicker/types.ts
Normal 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';
|
||||
}
|
||||
34
src/components/TradeDatePicker/utils.ts
Normal file
34
src/components/TradeDatePicker/utils.ts
Normal 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());
|
||||
};
|
||||
@@ -1467,7 +1467,6 @@ const ConceptCenter = () => {
|
||||
}}
|
||||
latestTradeDate={latestTradeDate}
|
||||
label="交易日期"
|
||||
isDarkMode={true}
|
||||
showLatestTradeDateTip={false}
|
||||
/>
|
||||
|
||||
|
||||
@@ -440,7 +440,6 @@ const HotspotOverview = ({ selectedDate, onDateChange, minDate, maxDate }) => {
|
||||
latestTradeDate={null}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
isDarkMode={true}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user