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 {
|
* TradeDatePicker 交易日期选择器组件
|
||||||
HStack,
|
*/
|
||||||
Input,
|
|
||||||
Text,
|
import React, { memo, useCallback, useMemo, useEffect } from 'react';
|
||||||
Icon,
|
import { HStack, Input, Text, Icon, Tooltip } from '@chakra-ui/react';
|
||||||
Tooltip,
|
|
||||||
useColorModeValue,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { Info, Calendar } from 'lucide-react';
|
import { Info, Calendar } from 'lucide-react';
|
||||||
|
|
||||||
export interface TradeDatePickerProps {
|
import type { TradeDatePickerProps } from './types';
|
||||||
/** 当前选中的日期 */
|
import { COLORS, INPUT_HOVER_STYLE, SIZE_CONFIG } from './theme';
|
||||||
value: Date | null;
|
import { formatDateToString, formatDateToChinese, getTodayString } from './utils';
|
||||||
/** 日期变化回调 */
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
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> = ({
|
const TradeDatePicker: React.FC<TradeDatePickerProps> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -50,121 +60,69 @@ const TradeDatePicker: React.FC<TradeDatePickerProps> = ({
|
|||||||
label = '交易日期',
|
label = '交易日期',
|
||||||
inputWidth = { base: '100%', lg: '200px' },
|
inputWidth = { base: '100%', lg: '200px' },
|
||||||
showIcon = true,
|
showIcon = true,
|
||||||
isDarkMode = false,
|
|
||||||
showLatestTradeDateTip = true,
|
showLatestTradeDateTip = true,
|
||||||
|
size = 'md',
|
||||||
}) => {
|
}) => {
|
||||||
// 颜色主题 - 支持 isDarkMode 强制覆盖
|
const sizeConfig = SIZE_CONFIG[size];
|
||||||
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 darkModeColors = {
|
useEffect(() => {
|
||||||
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(() => {
|
|
||||||
if (value === null && defaultDate) {
|
if (value === null && defaultDate) {
|
||||||
onChange(defaultDate);
|
onChange(defaultDate);
|
||||||
}
|
}
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 处理日期变化
|
const handleDateChange = useCallback(
|
||||||
const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const dateStr = e.target.value;
|
if (e.target.value) {
|
||||||
if (dateStr) {
|
onChange(new Date(e.target.value));
|
||||||
const date = new Date(dateStr);
|
}
|
||||||
onChange(date);
|
},
|
||||||
}
|
[onChange]
|
||||||
};
|
);
|
||||||
|
|
||||||
// 格式化日期为 YYYY-MM-DD
|
const { minDateStr, maxDateStr } = useMemo(
|
||||||
const formatDateValue = (date: Date | null): string => {
|
() => ({
|
||||||
if (!date) return '';
|
minDateStr: minDate ? formatDateToString(minDate) : undefined,
|
||||||
return date.toISOString().split('T')[0];
|
maxDateStr: maxDate ? formatDateToString(maxDate) : getTodayString(),
|
||||||
};
|
}),
|
||||||
|
[minDate, maxDate]
|
||||||
|
);
|
||||||
|
|
||||||
// 计算日期范围
|
const valueStr = useMemo(() => formatDateToString(value), [value]);
|
||||||
const minDateStr = minDate ? formatDateValue(minDate) : undefined;
|
|
||||||
const maxDateStr = maxDate
|
|
||||||
? formatDateValue(maxDate)
|
|
||||||
: new Date().toISOString().split('T')[0];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 标签 */}
|
<DateLabel
|
||||||
<HStack spacing={3}>
|
label={label}
|
||||||
{showIcon && <Icon as={Calendar} color={iconColor} boxSize={5} />}
|
showIcon={showIcon}
|
||||||
<Text fontWeight="bold" color={labelColor}>
|
iconSize={sizeConfig.iconSize}
|
||||||
{label}:
|
spacing={sizeConfig.spacing}
|
||||||
</Text>
|
/>
|
||||||
</HStack>
|
|
||||||
|
|
||||||
{/* 日期输入框 */}
|
|
||||||
<Input
|
<Input
|
||||||
type="date"
|
type="date"
|
||||||
value={formatDateValue(value)}
|
value={valueStr}
|
||||||
onChange={handleDateChange}
|
onChange={handleDateChange}
|
||||||
min={minDateStr}
|
min={minDateStr}
|
||||||
max={maxDateStr}
|
max={maxDateStr}
|
||||||
width={inputWidth}
|
width={inputWidth}
|
||||||
|
height={sizeConfig.inputHeight}
|
||||||
|
fontSize={sizeConfig.fontSize}
|
||||||
focusBorderColor="purple.400"
|
focusBorderColor="purple.400"
|
||||||
borderColor={inputBorderColor}
|
borderColor={COLORS.inputBorder}
|
||||||
borderRadius="lg"
|
borderRadius="lg"
|
||||||
fontWeight="medium"
|
fontWeight="medium"
|
||||||
bg={isDarkMode ? darkModeColors.inputBg : undefined}
|
transition="all 0.2s"
|
||||||
color={isDarkMode ? darkModeColors.inputColor : undefined}
|
_hover={INPUT_HOVER_STYLE}
|
||||||
_hover={{ borderColor: isDarkMode ? 'purple.400' : 'purple.300' }}
|
|
||||||
sx={isDarkMode ? {
|
|
||||||
'&::-webkit-calendar-picker-indicator': {
|
|
||||||
filter: 'invert(1)',
|
|
||||||
},
|
|
||||||
} : undefined}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 最新交易日期提示 - 靠右显示,样式更低调避免误认为按钮 */}
|
|
||||||
{showLatestTradeDateTip && latestTradeDate && (
|
{showLatestTradeDateTip && latestTradeDate && (
|
||||||
<Tooltip label="数据库中最新的交易日期">
|
<LatestDateTip date={latestTradeDate} />
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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}
|
latestTradeDate={latestTradeDate}
|
||||||
label="交易日期"
|
label="交易日期"
|
||||||
isDarkMode={true}
|
|
||||||
showLatestTradeDateTip={false}
|
showLatestTradeDateTip={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -440,7 +440,6 @@ const HotspotOverview = ({ selectedDate, onDateChange, minDate, maxDate }) => {
|
|||||||
latestTradeDate={null}
|
latestTradeDate={null}
|
||||||
minDate={minDate}
|
minDate={minDate}
|
||||||
maxDate={maxDate}
|
maxDate={maxDate}
|
||||||
isDarkMode={true}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user