Files
vf_react/src/views/StockOverview/components/MarketHeatmap/utils.ts
zdl 0eb1d00482 refactor(MarketHeatmap): TypeScript 重构与模块化优化
目录结构拆分:
- types.ts: HeatmapDataItem, MarketHeatmapProps, TreeNodeData 等类型定义
- styles.ts: 颜色常量、ECharts 配置常量、涨跌幅阈值
- utils.ts: getMarketCapRange, getChangeColor, buildTreeData, tooltip 格式化函数
- components/HeatmapLegend.tsx: 图例原子组件

性能优化:
- 使用 useMemo 缓存树图数据构建和 ECharts 配置
- HeatmapLegend 使用 memo 包装

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-31 17:52:39 +08:00

106 lines
3.7 KiB
TypeScript

/**
* MarketHeatmap 工具函数
*/
import type { HeatmapDataItem, TreeNodeData, MarketCapRange } from './types';
import { COLORS, CHANGE_THRESHOLDS } from './styles';
/**
* 获取市值区间标签
*/
export const getMarketCapRange = (cap: number): MarketCapRange => {
if (cap >= 1000) return '超大盘股(>1000亿)';
if (cap >= 500) return '大盘股(500-1000亿)';
if (cap >= 100) return '中盘股(100-500亿)';
if (cap >= 50) return '小盘股(50-100亿)';
return '微盘股(<50亿)';
};
/**
* 根据涨跌幅计算颜色
*/
export const getChangeColor = (change: number): string => {
if (change > 0) {
const intensity = Math.min(change / CHANGE_THRESHOLDS.maxChange, 1);
const opacity = CHANGE_THRESHOLDS.upBaseOpacity + intensity * CHANGE_THRESHOLDS.upMaxOpacity;
return `rgba(255, 77, 77, ${opacity})`;
}
if (change < 0) {
const intensity = Math.min(Math.abs(change) / CHANGE_THRESHOLDS.maxChange, 1);
const opacity = CHANGE_THRESHOLDS.downBaseOpacity + intensity * CHANGE_THRESHOLDS.downMaxOpacity;
return `rgba(34, 197, 94, ${opacity})`;
}
return COLORS.neutral;
};
/**
* 按市值分组数据
*/
export const groupByMarketCap = (data: HeatmapDataItem[]): Record<MarketCapRange, HeatmapDataItem[]> => {
const grouped: Record<string, HeatmapDataItem[]> = {};
data.forEach(item => {
const range = getMarketCapRange(item.market_cap);
if (!grouped[range]) {
grouped[range] = [];
}
grouped[range].push(item);
});
return grouped as Record<MarketCapRange, HeatmapDataItem[]>;
};
/**
* 构建树图数据
*/
export const buildTreeData = (data: HeatmapDataItem[]): TreeNodeData[] => {
const groupedData = groupByMarketCap(data);
return Object.entries(groupedData).map(([range, stocks]) => ({
name: range,
children: stocks.map(stock => ({
name: stock.stock_name,
value: Math.abs(stock.market_cap),
change: stock.change_percent || 0,
code: stock.stock_code,
amount: stock.amount,
industry: stock.industry,
province: stock.province,
itemStyle: { color: getChangeColor(stock.change_percent || 0) },
})),
}));
};
/**
* 生成分组 tooltip HTML
*/
export const formatGroupTooltip = (data: TreeNodeData): string => {
const totalMarketCap = data.children?.reduce((sum, item) => sum + (item.value || 0), 0) || 0;
return `
<div style="padding: 10px; color: white;">
<div style="font-weight: bold; margin-bottom: 6px; font-size: 14px; color: ${COLORS.gold};">${data.name}</div>
<div style="color: #ccc;">包含 ${data.children?.length || 0} 只股票</div>
<div style="color: #ccc;">总市值: <span style="color: ${COLORS.gold}; font-weight: bold;">${totalMarketCap.toFixed(2)}</span> 亿元</div>
</div>
`;
};
/**
* 生成股票 tooltip HTML
*/
export const formatStockTooltip = (data: TreeNodeData): string => {
const changeColor = (data.change || 0) > 0 ? COLORS.up : COLORS.down;
const changeSign = (data.change || 0) > 0 ? '+' : '';
return `
<div style="padding: 10px; color: white;">
<div style="font-weight: bold; margin-bottom: 6px; font-size: 14px; color: ${COLORS.gold};">${data.name}</div>
<div style="color: #ccc;">代码: ${data.code || '-'}</div>
<div style="color: #ccc;">涨跌幅: <span style="color: ${changeColor}; font-weight: bold;">
${changeSign}${data.change?.toFixed(2) || 0}%
</span></div>
<div style="color: #ccc;">市值: <span style="font-weight: bold;">${data.value?.toFixed(2) || 0}</span> 亿元</div>
<div style="color: #ccc;">成交额: <span style="font-weight: bold;">${data.amount?.toFixed(2) || 0}</span> 亿元</div>
<div style="color: #ccc;">行业: ${data.industry || '未知'}</div>
</div>
`;
};