refactor(StockOverviewHeader): 模块化重构与性能优化
目录结构拆分: - types.ts: 提取 6 个 TypeScript 接口定义 - styles.ts: 提取颜色常量和样式对象 - utils.ts: 提取趋势计算函数 (getAmountTrend, getMarketCapTrend) - components/StatCard.tsx: 提取统计卡片原子组件 性能优化: - StatCard 使用 memo + useMemo 缓存趋势图标和颜色 - 主组件使用 useMemo 缓存所有计算值 - 趋势函数外移避免每次渲染重新创建 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* StatCard - 统计卡片原子组件
|
||||
* 用于展示单个统计指标,支持趋势显示和进度条
|
||||
*/
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
Icon,
|
||||
HStack,
|
||||
VStack,
|
||||
Progress,
|
||||
Skeleton,
|
||||
} from '@chakra-ui/react';
|
||||
import { ArrowUp, ArrowDown } from 'lucide-react';
|
||||
import type { StatCardProps } from '../types';
|
||||
import { COLORS, statCardStyles, watermarkIconStyles } from '../styles';
|
||||
|
||||
const StatCard: React.FC<StatCardProps> = memo(({
|
||||
label,
|
||||
value,
|
||||
valueColor,
|
||||
icon: IconComponent,
|
||||
iconColor = COLORS.gold,
|
||||
trend,
|
||||
progressBar,
|
||||
helpText,
|
||||
}) => {
|
||||
// 缓存趋势图标计算
|
||||
const TrendIcon = useMemo(() => {
|
||||
if (!trend) return null;
|
||||
if (trend.direction === 'up') return ArrowUp;
|
||||
if (trend.direction === 'down') return ArrowDown;
|
||||
return null;
|
||||
}, [trend]);
|
||||
|
||||
// 缓存趋势颜色
|
||||
const trendColor = useMemo(() => {
|
||||
if (!trend) return undefined;
|
||||
return trend.direction === 'up' ? COLORS.up : COLORS.down;
|
||||
}, [trend]);
|
||||
|
||||
return (
|
||||
<Box sx={statCardStyles}>
|
||||
{/* 水印图标 */}
|
||||
<Icon
|
||||
as={IconComponent}
|
||||
sx={watermarkIconStyles}
|
||||
color={iconColor}
|
||||
/>
|
||||
|
||||
<VStack align="start" spacing={2}>
|
||||
<Text fontSize="xs" color={COLORS.subText} fontWeight="medium">
|
||||
{label}
|
||||
</Text>
|
||||
|
||||
{value === null ? (
|
||||
<Skeleton height="28px" width="80px" />
|
||||
) : (
|
||||
<HStack spacing={2} align="baseline">
|
||||
<Text
|
||||
fontSize="xl"
|
||||
fontWeight="bold"
|
||||
color={valueColor || COLORS.text}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
{trend && TrendIcon && (
|
||||
<HStack spacing={1} fontSize="xs" color={trendColor}>
|
||||
<Icon as={TrendIcon} boxSize={3} />
|
||||
<Text>{trend.percent.toFixed(1)}%</Text>
|
||||
{trend.label && (
|
||||
<Text color={trendColor}>
|
||||
{trend.label}
|
||||
</Text>
|
||||
)}
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{helpText && (
|
||||
<Text fontSize="xs" color={COLORS.subText}>
|
||||
{helpText}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{progressBar && (
|
||||
<Box w="100%" mt={1}>
|
||||
<Progress
|
||||
value={(progressBar.value / (progressBar.value + progressBar.total)) * 100}
|
||||
size="xs"
|
||||
borderRadius="full"
|
||||
bg={progressBar.negativeColor}
|
||||
sx={{
|
||||
'& > div': {
|
||||
bg: progressBar.positiveColor,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
StatCard.displayName = 'StatCard';
|
||||
|
||||
export default StatCard;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as StatCard } from './StatCard';
|
||||
222
src/views/StockOverview/components/StockOverviewHeader/index.tsx
Normal file
222
src/views/StockOverview/components/StockOverviewHeader/index.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* StockOverviewHeader - 个股中心头部组件
|
||||
* 左右分栏:左侧标题+统计指标,右侧热力图
|
||||
*
|
||||
* 优化点:
|
||||
* 1. 类型拆分到 types.ts
|
||||
* 2. 样式常量拆分到 styles.ts
|
||||
* 3. 工具函数拆分到 utils.ts
|
||||
* 4. StatCard 拆分为原子组件
|
||||
* 5. 使用 useMemo 缓存计算结果
|
||||
*/
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Heading,
|
||||
Text,
|
||||
SimpleGrid,
|
||||
Icon,
|
||||
HStack,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
TrendingUp,
|
||||
Zap,
|
||||
Scale,
|
||||
Banknote,
|
||||
Wallet,
|
||||
Rocket,
|
||||
} from 'lucide-react';
|
||||
import MarketHeatmap from '../MarketHeatmap';
|
||||
|
||||
import type { StockOverviewHeaderProps } from './types';
|
||||
import { StatCard } from './components';
|
||||
import { COLORS, heatmapContainerStyles } from './styles';
|
||||
import { getAmountTrend, getMarketCapTrend, formatChangePercent, formatAmount } from './utils';
|
||||
|
||||
const StockOverviewHeader: React.FC<StockOverviewHeaderProps> = ({
|
||||
hotspotData,
|
||||
limitStats,
|
||||
marketStats,
|
||||
heatmapData,
|
||||
loadingHeatmap,
|
||||
onStockClick,
|
||||
}) => {
|
||||
// 缓存趋势计算
|
||||
const amountTrend = useMemo(() => getAmountTrend(marketStats), [marketStats]);
|
||||
const marketCapTrend = useMemo(() => getMarketCapTrend(marketStats), [marketStats]);
|
||||
|
||||
// 缓存涨跌幅显示值
|
||||
const changePercentValue = useMemo(() => {
|
||||
if (hotspotData?.index?.change_pct == null) return null;
|
||||
return formatChangePercent(hotspotData.index.change_pct);
|
||||
}, [hotspotData?.index?.change_pct]);
|
||||
|
||||
const changePercentColor = useMemo(() => {
|
||||
return (hotspotData?.index?.change_pct ?? 0) >= 0 ? COLORS.up : COLORS.down;
|
||||
}, [hotspotData?.index?.change_pct]);
|
||||
|
||||
// 缓存涨停/跌停值
|
||||
const limitValue = useMemo(() => {
|
||||
if (limitStats.limitUpCount > 0 || limitStats.limitDownCount > 0) {
|
||||
return `${limitStats.limitUpCount}/${limitStats.limitDownCount}`;
|
||||
}
|
||||
return null;
|
||||
}, [limitStats.limitUpCount, limitStats.limitDownCount]);
|
||||
|
||||
const limitProgressBar = useMemo(() => {
|
||||
if (limitStats.limitUpCount > 0 || limitStats.limitDownCount > 0) {
|
||||
return {
|
||||
value: limitStats.limitUpCount,
|
||||
total: limitStats.limitDownCount || 1,
|
||||
positiveColor: COLORS.up,
|
||||
negativeColor: COLORS.down,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}, [limitStats.limitUpCount, limitStats.limitDownCount]);
|
||||
|
||||
// 缓存多空对比值
|
||||
const bullBearValue = useMemo(() => {
|
||||
if (marketStats?.rising_count && marketStats?.falling_count) {
|
||||
return `${marketStats.rising_count}/${marketStats.falling_count}`;
|
||||
}
|
||||
return null;
|
||||
}, [marketStats?.rising_count, marketStats?.falling_count]);
|
||||
|
||||
const bullBearProgressBar = useMemo(() => {
|
||||
if (marketStats?.rising_count && marketStats?.falling_count) {
|
||||
return {
|
||||
value: marketStats.rising_count,
|
||||
total: marketStats.falling_count,
|
||||
positiveColor: COLORS.up,
|
||||
negativeColor: COLORS.down,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}, [marketStats?.rising_count, marketStats?.falling_count]);
|
||||
|
||||
// 缓存成交额和市值显示值
|
||||
const amountValue = useMemo(() => {
|
||||
return marketStats?.total_amount ? formatAmount(marketStats.total_amount) : null;
|
||||
}, [marketStats?.total_amount]);
|
||||
|
||||
const marketCapValue = useMemo(() => {
|
||||
return marketStats?.total_market_cap ? formatAmount(marketStats.total_market_cap) : null;
|
||||
}, [marketStats?.total_market_cap]);
|
||||
|
||||
// 缓存连板龙头显示值
|
||||
const continuousValue = useMemo(() => {
|
||||
return limitStats.limitUpCount > 0 ? `${limitStats.limitUpCount}只` : '暂无';
|
||||
}, [limitStats.limitUpCount]);
|
||||
|
||||
const continuousHelpText = useMemo(() => {
|
||||
return limitStats.maxContinuousDays > 1
|
||||
? `最高${limitStats.maxContinuousDays}天`
|
||||
: undefined;
|
||||
}, [limitStats.maxContinuousDays]);
|
||||
|
||||
return (
|
||||
<Box position="relative" zIndex={1} pt={8} pb={6} px={6}>
|
||||
<Flex
|
||||
direction={{ base: 'column', md: 'row' }}
|
||||
gap={6}
|
||||
align="stretch"
|
||||
>
|
||||
{/* 左侧:标题 + 统计指标 (40%) */}
|
||||
<Box flex="4" minW={0}>
|
||||
{/* 标题区 */}
|
||||
<HStack spacing={3} mb={4}>
|
||||
<Icon as={TrendingUp} boxSize={8} color={COLORS.gold} />
|
||||
<Heading
|
||||
size="xl"
|
||||
bgGradient={`linear(to-r, ${COLORS.gold}, white)`}
|
||||
bgClip="text"
|
||||
>
|
||||
个股中心
|
||||
</Heading>
|
||||
</HStack>
|
||||
<Text color={COLORS.subText} fontSize="md" mb={6}>
|
||||
实时追踪市场动态,洞察投资机会
|
||||
</Text>
|
||||
|
||||
{/* 统计指标网格 */}
|
||||
<SimpleGrid columns={{ base: 2, md: 3 }} spacing={4}>
|
||||
{/* 大盘涨跌幅 */}
|
||||
<StatCard
|
||||
label="大盘涨跌幅"
|
||||
value={changePercentValue}
|
||||
valueColor={changePercentColor}
|
||||
icon={TrendingUp}
|
||||
/>
|
||||
|
||||
{/* 涨停/跌停 */}
|
||||
<StatCard
|
||||
label="涨停/跌停"
|
||||
value={limitValue}
|
||||
icon={Zap}
|
||||
iconColor={COLORS.up}
|
||||
progressBar={limitProgressBar}
|
||||
/>
|
||||
|
||||
{/* 多空对比 */}
|
||||
<StatCard
|
||||
label="多空对比"
|
||||
value={bullBearValue}
|
||||
icon={Scale}
|
||||
progressBar={bullBearProgressBar}
|
||||
/>
|
||||
|
||||
{/* 今日成交额 */}
|
||||
<StatCard
|
||||
label="今日成交额"
|
||||
value={amountValue}
|
||||
icon={Banknote}
|
||||
trend={amountTrend}
|
||||
/>
|
||||
|
||||
{/* A股总市值 */}
|
||||
<StatCard
|
||||
label="A股总市值"
|
||||
value={marketCapValue}
|
||||
icon={Wallet}
|
||||
trend={marketCapTrend}
|
||||
/>
|
||||
|
||||
{/* 连板龙头 */}
|
||||
<StatCard
|
||||
label="连板龙头"
|
||||
value={continuousValue}
|
||||
valueColor={COLORS.warning}
|
||||
icon={Rocket}
|
||||
iconColor={COLORS.warning}
|
||||
helpText={continuousHelpText}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
|
||||
{/* 右侧:热力图 (60%) */}
|
||||
<Box flex="6" minH="400px" sx={heatmapContainerStyles}>
|
||||
<MarketHeatmap
|
||||
data={heatmapData}
|
||||
height="384px"
|
||||
loading={loadingHeatmap}
|
||||
onStockClick={onStockClick}
|
||||
showLegend={true}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(StockOverviewHeader);
|
||||
|
||||
// 导出类型供外部使用
|
||||
export type {
|
||||
StockOverviewHeaderProps,
|
||||
HotspotData,
|
||||
LimitStats,
|
||||
MarketStats,
|
||||
HeatmapDataItem,
|
||||
} from './types';
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* StockOverviewHeader 样式常量
|
||||
*/
|
||||
import type { SystemStyleObject } from '@chakra-ui/react';
|
||||
|
||||
/** 颜色常量 */
|
||||
export const COLORS = {
|
||||
// 主题色
|
||||
gold: '#8b5cf6',
|
||||
|
||||
// 文字颜色
|
||||
text: 'rgba(255, 255, 255, 0.95)',
|
||||
subText: 'rgba(255, 255, 255, 0.6)',
|
||||
|
||||
// 涨跌颜色
|
||||
up: '#ff4d4d',
|
||||
down: '#22c55e',
|
||||
|
||||
// 警告/连板颜色
|
||||
warning: '#f59e0b',
|
||||
|
||||
// 背景与边框
|
||||
cardBg: 'rgba(255, 255, 255, 0.03)',
|
||||
border: 'rgba(255, 255, 255, 0.08)',
|
||||
} as const;
|
||||
|
||||
/** StatCard 卡片样式 */
|
||||
export const statCardStyles: SystemStyleObject = {
|
||||
bg: COLORS.cardBg,
|
||||
borderWidth: '1px',
|
||||
borderColor: COLORS.border,
|
||||
borderRadius: 'xl',
|
||||
p: 4,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
transition: 'all 0.2s',
|
||||
_hover: {
|
||||
borderColor: `${COLORS.gold}50`,
|
||||
transform: 'translateY(-2px)',
|
||||
},
|
||||
};
|
||||
|
||||
/** 水印图标样式 */
|
||||
export const watermarkIconStyles: SystemStyleObject = {
|
||||
position: 'absolute',
|
||||
right: 2,
|
||||
bottom: 2,
|
||||
boxSize: 12,
|
||||
opacity: 0.1,
|
||||
};
|
||||
|
||||
/** 热力图容器样式 */
|
||||
export const heatmapContainerStyles: SystemStyleObject = {
|
||||
bg: COLORS.cardBg,
|
||||
borderWidth: '1px',
|
||||
borderColor: COLORS.border,
|
||||
borderRadius: '2xl',
|
||||
overflow: 'hidden',
|
||||
p: 2,
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* StockOverviewHeader 类型定义
|
||||
*/
|
||||
|
||||
/** 趋势方向类型 */
|
||||
export type TrendDirection = 'up' | 'down' | 'flat';
|
||||
|
||||
/** 热点数据 */
|
||||
export interface HotspotData {
|
||||
index?: {
|
||||
change_pct?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/** 涨跌停统计 */
|
||||
export interface LimitStats {
|
||||
limitUpCount: number;
|
||||
limitDownCount: number;
|
||||
continuousLimitCount: number;
|
||||
maxContinuousDays: number;
|
||||
}
|
||||
|
||||
/** 市场统计数据 */
|
||||
export interface MarketStats {
|
||||
rising_count?: number;
|
||||
falling_count?: number;
|
||||
total_amount?: number;
|
||||
total_market_cap?: number;
|
||||
yesterday?: {
|
||||
total_amount?: number;
|
||||
total_market_cap?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/** 热力图数据项 */
|
||||
export interface HeatmapDataItem {
|
||||
stock_code: string;
|
||||
stock_name: string;
|
||||
change_percent: number;
|
||||
market_cap: number;
|
||||
amount: number;
|
||||
industry?: string;
|
||||
province?: string;
|
||||
}
|
||||
|
||||
/** StockOverviewHeader 组件 Props */
|
||||
export interface StockOverviewHeaderProps {
|
||||
hotspotData: HotspotData | null;
|
||||
limitStats: LimitStats;
|
||||
marketStats: MarketStats | null;
|
||||
heatmapData: HeatmapDataItem[];
|
||||
loadingHeatmap: boolean;
|
||||
onStockClick?: (code: string, name: string) => void;
|
||||
}
|
||||
|
||||
/** 趋势配置 */
|
||||
export interface TrendConfig {
|
||||
direction: TrendDirection;
|
||||
percent: number;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/** 进度条配置 */
|
||||
export interface ProgressBarConfig {
|
||||
value: number;
|
||||
total: number;
|
||||
positiveColor: string;
|
||||
negativeColor: string;
|
||||
}
|
||||
|
||||
/** StatCard 组件 Props */
|
||||
export interface StatCardProps {
|
||||
label: string;
|
||||
value: string | null;
|
||||
valueColor?: string;
|
||||
icon: React.ElementType;
|
||||
iconColor?: string;
|
||||
trend?: TrendConfig;
|
||||
progressBar?: ProgressBarConfig;
|
||||
helpText?: string;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* StockOverviewHeader 工具函数
|
||||
*/
|
||||
import type { MarketStats, TrendConfig, TrendDirection } from './types';
|
||||
|
||||
/**
|
||||
* 获取趋势方向
|
||||
* @param change 变化百分比
|
||||
* @returns 趋势方向
|
||||
*/
|
||||
export const getTrendDirection = (change: number): TrendDirection => {
|
||||
if (change > 0.01) return 'up';
|
||||
if (change < -0.01) return 'down';
|
||||
return 'flat';
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算成交额趋势
|
||||
* @param marketStats 市场统计数据
|
||||
* @returns 趋势配置
|
||||
*/
|
||||
export const getAmountTrend = (marketStats: MarketStats | null): TrendConfig | undefined => {
|
||||
if (!marketStats?.yesterday?.total_amount || !marketStats.total_amount) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const change =
|
||||
((marketStats.total_amount - marketStats.yesterday.total_amount) /
|
||||
marketStats.yesterday.total_amount) *
|
||||
100;
|
||||
|
||||
const direction = getTrendDirection(change);
|
||||
|
||||
return {
|
||||
direction,
|
||||
percent: Math.abs(change),
|
||||
label: change > 5 ? '放量' : change < -5 ? '缩量' : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算市值趋势
|
||||
* @param marketStats 市场统计数据
|
||||
* @returns 趋势配置
|
||||
*/
|
||||
export const getMarketCapTrend = (marketStats: MarketStats | null): TrendConfig | undefined => {
|
||||
if (!marketStats?.yesterday?.total_market_cap || !marketStats.total_market_cap) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const change =
|
||||
((marketStats.total_market_cap - marketStats.yesterday.total_market_cap) /
|
||||
marketStats.yesterday.total_market_cap) *
|
||||
100;
|
||||
|
||||
const direction = getTrendDirection(change);
|
||||
|
||||
return {
|
||||
direction,
|
||||
percent: Math.abs(change),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化涨跌幅显示
|
||||
* @param value 涨跌幅数值
|
||||
* @returns 格式化后的字符串
|
||||
*/
|
||||
export const formatChangePercent = (value: number): string => {
|
||||
const sign = value >= 0 ? '+' : '';
|
||||
return `${sign}${value.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化金额(单位:万亿)
|
||||
* @param value 金额数值(单位:亿)
|
||||
* @returns 格式化后的字符串
|
||||
*/
|
||||
export const formatAmount = (value: number): string => {
|
||||
return `${(value / 10000).toFixed(1)}万亿`;
|
||||
};
|
||||
Reference in New Issue
Block a user