// src/components/StockChangeIndicators.js
// 股票涨跌幅指标组件(通用)
import React from 'react';
import { Flex, Box, Text, useColorModeValue } from '@chakra-ui/react';
import { TriangleUpIcon, TriangleDownIcon } from '@chakra-ui/icons';
import { getChangeColor } from '../utils/colorUtils';
/**
* 股票涨跌幅指标组件(3个指标:平均超额、最大超额、超预期得分)
* @param {Object} props
* @param {number} props.avgChange - 平均超额涨幅
* @param {number} props.maxChange - 最大超额涨幅
* @param {number} props.expectationScore - 超预期得分(0-100)
* @param {'default'|'comfortable'|'large'} props.size - 尺寸模式:default=紧凑,comfortable=舒适(事件列表),large=大卡片(详情面板)
*/
const StockChangeIndicators = ({
avgChange,
maxChange,
expectationScore,
size = 'default',
}) => {
const isLarge = size === 'large';
const isComfortable = size === 'comfortable';
const isDefault = size === 'default';
// 根据涨跌幅获取数字颜色(动态深浅)
const getNumberColor = (value) => {
if (value == null) {
return useColorModeValue('gray.700', 'gray.400');
}
// 使用动态颜色函数
return getChangeColor(value);
};
// 根据涨跌幅获取背景色(永远比文字色浅)
const getBgColor = (value) => {
if (value == null) {
return useColorModeValue('gray.50', 'gray.800');
}
// 0值使用中性灰色背景
if (value === 0) {
return useColorModeValue('gray.50', 'gray.800');
}
// 统一背景色:上涨红色系,下跌绿色系
return value > 0
? useColorModeValue('red.50', 'red.900')
: useColorModeValue('green.50', 'green.900');
};
// 根据涨跌幅获取边框色(比背景深,比文字浅)
const getBorderColor = (value) => {
if (value == null) {
return useColorModeValue('gray.200', 'gray.700');
}
// 0值使用中性灰色边框
if (value === 0) {
return useColorModeValue('gray.200', 'gray.700');
}
// 统一边框色:上涨红色系,下跌绿色系
return value > 0
? useColorModeValue('red.200', 'red.700')
: useColorModeValue('green.200', 'green.700');
};
// 渲染单个指标
const renderIndicator = (label, value) => {
if (value == null) return null;
const sign = value > 0 ? '+' : '-';
// 0值显示为 "0",其他值显示两位小数
const numStr = value === 0 ? '0' : Math.abs(value).toFixed(2);
const numberColor = getNumberColor(value);
const bgColor = getBgColor(value);
const borderColor = getBorderColor(value);
const labelColor = useColorModeValue('gray.600', 'gray.400');
return (
{/* Large 和 Default 模式:标签单独一行 */}
{(isLarge || isDefault) && (
{label}
)}
{/* 数值 + 图标 */}
{/* 三角形图标 */}
{value !== 0 && (
value > 0 ? (
) : (
)
)}
{/* 数字 */}
{/* Comfortable 模式:标签和数字在同一行 */}
{!isLarge && !isDefault && (
{label}{' '}
)}
{sign}{numStr}
%
);
};
// 渲染超预期得分指标(特殊样式,分数而非百分比)
const renderScoreIndicator = (label, score) => {
if (score == null) return null;
const labelColor = useColorModeValue('gray.600', 'gray.400');
// 根据分数确定颜色:>=60红色,>=40橙色,>=20蓝色,其他灰色
const getScoreColor = (s) => {
if (s >= 60) return useColorModeValue('red.600', 'red.400');
if (s >= 40) return useColorModeValue('orange.600', 'orange.400');
if (s >= 20) return useColorModeValue('blue.600', 'blue.400');
return useColorModeValue('gray.600', 'gray.400');
};
const getScoreBgColor = (s) => {
if (s >= 60) return useColorModeValue('red.50', 'red.900');
if (s >= 40) return useColorModeValue('orange.50', 'orange.900');
if (s >= 20) return useColorModeValue('blue.50', 'blue.900');
return useColorModeValue('gray.50', 'gray.800');
};
const getScoreBorderColor = (s) => {
if (s >= 60) return useColorModeValue('red.200', 'red.700');
if (s >= 40) return useColorModeValue('orange.200', 'orange.700');
if (s >= 20) return useColorModeValue('blue.200', 'blue.700');
return useColorModeValue('gray.200', 'gray.700');
};
const scoreColor = getScoreColor(score);
const bgColor = getScoreBgColor(score);
const borderColor = getScoreBorderColor(score);
return (
{/* Large 和 Default 模式:标签单独一行 */}
{(isLarge || isDefault) && (
{label}
)}
{/* 数值 */}
{/* Comfortable 模式:标签和数字在同一行 */}
{!isLarge && !isDefault && (
{label}
)}
{Math.round(score)}
分
);
};
// 如果没有任何数据,不渲染
if (avgChange == null && maxChange == null && expectationScore == null) {
return null;
}
return (
{renderIndicator('平均超额', avgChange)}
{renderIndicator('最大超额', maxChange)}
{renderScoreIndicator('超预期', expectationScore)}
);
};
export default StockChangeIndicators;