优化: - 背景色统一使用 50 最浅色 (red.50/orange.50/green.50/teal.50) - 边框色根据涨跌幅大小动态调整 (100-200 级别) - 确保背景 < 边框 < 文字的颜色深度层次 - 提升视觉对比度和可读性 - 更新注释说明颜色逻辑 修改文件: - src/components/StockChangeIndicators.js 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
169 lines
6.7 KiB
JavaScript
169 lines
6.7 KiB
JavaScript
// src/components/StockChangeIndicators.js
|
||
// 股票涨跌幅指标组件(通用)
|
||
|
||
import React from 'react';
|
||
import { Flex, Box, Text, useColorModeValue } from '@chakra-ui/react';
|
||
|
||
/**
|
||
* 股票涨跌幅指标组件(3分天下布局)
|
||
* @param {Object} props
|
||
* @param {number} props.avgChange - 平均涨跌幅
|
||
* @param {number} props.maxChange - 最大涨跌幅
|
||
* @param {number} props.weekChange - 周涨跌幅
|
||
*/
|
||
const StockChangeIndicators = ({
|
||
avgChange,
|
||
maxChange,
|
||
weekChange,
|
||
}) => {
|
||
// 根据涨跌幅获取数字颜色(多颜色梯度:5级分级)
|
||
const getNumberColor = (value) => {
|
||
if (value == null) {
|
||
return useColorModeValue('gray.700', 'gray.400');
|
||
}
|
||
|
||
// 0值使用中性灰色
|
||
if (value === 0) {
|
||
return 'gray.700';
|
||
}
|
||
|
||
const absValue = Math.abs(value);
|
||
const isPositive = value > 0;
|
||
|
||
if (isPositive) {
|
||
// 上涨:红色系 → 橙色系
|
||
if (absValue >= 10) return 'red.900'; // 10%以上:最深红
|
||
if (absValue >= 5) return 'red.700'; // 5-10%:深红
|
||
if (absValue >= 3) return 'red.500'; // 3-5%:中红
|
||
if (absValue >= 1) return 'orange.600'; // 1-3%:橙色
|
||
return 'orange.400'; // 0-1%:浅橙
|
||
} else {
|
||
// 下跌:绿色系 → 青色系
|
||
if (absValue >= 10) return 'green.900'; // -10%以下:最深绿
|
||
if (absValue >= 5) return 'green.700'; // -10% ~ -5%:深绿
|
||
if (absValue >= 3) return 'green.500'; // -5% ~ -3%:中绿
|
||
if (absValue >= 1) return 'teal.600'; // -3% ~ -1%:青色
|
||
return 'teal.400'; // -1% ~ 0%:浅青
|
||
}
|
||
};
|
||
|
||
// 根据涨跌幅获取背景色(永远比文字色浅)
|
||
const getBgColor = (value) => {
|
||
if (value == null) {
|
||
return useColorModeValue('gray.50', 'gray.800');
|
||
}
|
||
|
||
// 0值使用中性灰色背景
|
||
if (value === 0) {
|
||
return useColorModeValue('gray.50', 'gray.800');
|
||
}
|
||
|
||
const absValue = Math.abs(value);
|
||
const isPositive = value > 0;
|
||
|
||
if (isPositive) {
|
||
// 上涨背景:红色系 → 橙色系(统一使用 50 最浅色)
|
||
if (absValue >= 10) return useColorModeValue('red.50', 'red.900');
|
||
if (absValue >= 5) return useColorModeValue('red.50', 'red.900');
|
||
if (absValue >= 3) return useColorModeValue('red.50', 'red.900');
|
||
if (absValue >= 1) return useColorModeValue('orange.50', 'orange.900');
|
||
return useColorModeValue('orange.50', 'orange.900');
|
||
} else {
|
||
// 下跌背景:绿色系 → 青色系(统一使用 50 最浅色)
|
||
if (absValue >= 10) return useColorModeValue('green.50', 'green.900');
|
||
if (absValue >= 5) return useColorModeValue('green.50', 'green.900');
|
||
if (absValue >= 3) return useColorModeValue('green.50', 'green.900');
|
||
if (absValue >= 1) return useColorModeValue('teal.50', 'teal.900');
|
||
return useColorModeValue('teal.50', 'teal.900');
|
||
}
|
||
};
|
||
|
||
// 根据涨跌幅获取边框色(比背景深,比文字浅)
|
||
const getBorderColor = (value) => {
|
||
if (value == null) {
|
||
return useColorModeValue('gray.200', 'gray.700');
|
||
}
|
||
|
||
// 0值使用中性灰色边框
|
||
if (value === 0) {
|
||
return useColorModeValue('gray.200', 'gray.700');
|
||
}
|
||
|
||
const absValue = Math.abs(value);
|
||
const isPositive = value > 0;
|
||
|
||
if (isPositive) {
|
||
// 上涨边框:红色系 → 橙色系(跟随文字深浅)
|
||
if (absValue >= 10) return useColorModeValue('red.200', 'red.800'); // 文字 red.900
|
||
if (absValue >= 5) return useColorModeValue('red.200', 'red.700'); // 文字 red.700
|
||
if (absValue >= 3) return useColorModeValue('red.100', 'red.600'); // 文字 red.500
|
||
if (absValue >= 1) return useColorModeValue('orange.200', 'orange.700'); // 文字 orange.600
|
||
return useColorModeValue('orange.100', 'orange.600'); // 文字 orange.400
|
||
} else {
|
||
// 下跌边框:绿色系 → 青色系(跟随文字深浅)
|
||
if (absValue >= 10) return useColorModeValue('green.200', 'green.800'); // 文字 green.900
|
||
if (absValue >= 5) return useColorModeValue('green.200', 'green.700'); // 文字 green.700
|
||
if (absValue >= 3) return useColorModeValue('green.100', 'green.600'); // 文字 green.500
|
||
if (absValue >= 1) return useColorModeValue('teal.200', 'teal.700'); // 文字 teal.600
|
||
return useColorModeValue('teal.100', 'teal.600'); // 文字 teal.400
|
||
}
|
||
};
|
||
|
||
// 渲染单个指标
|
||
const renderIndicator = (label, value) => {
|
||
if (value == null) return null;
|
||
|
||
const sign = value > 0 ? '+' : '';
|
||
// 0值显示为 "0",其他值显示一位小数
|
||
const numStr = value === 0 ? '0' : Math.abs(value).toFixed(1);
|
||
const numberColor = getNumberColor(value);
|
||
const bgColor = getBgColor(value);
|
||
const borderColor = getBorderColor(value);
|
||
const labelColor = useColorModeValue('gray.700', 'gray.400');
|
||
|
||
return (
|
||
<Box
|
||
bg={bgColor}
|
||
borderWidth="2px"
|
||
borderColor={borderColor}
|
||
borderRadius="md"
|
||
px={1.5}
|
||
py={0.5}
|
||
display="flex"
|
||
alignItems="center"
|
||
justifyContent="center"
|
||
>
|
||
<Text fontSize="xs" lineHeight="1.2">
|
||
<Text as="span" color={labelColor}>
|
||
{label}
|
||
</Text>
|
||
<Text as="span" color={labelColor}>
|
||
{sign}
|
||
</Text>
|
||
<Text as="span" fontWeight="bold" color={numberColor} fontSize="sm">
|
||
{value < 0 ? '-' : ''}{numStr}
|
||
</Text>
|
||
<Text as="span" color={labelColor}>
|
||
%
|
||
</Text>
|
||
</Text>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
// 如果没有任何数据,不渲染
|
||
if (avgChange == null && maxChange == null && weekChange == null) {
|
||
return null;
|
||
}
|
||
|
||
return (
|
||
<Flex width="100%" justify="space-between" align="center" gap={1}>
|
||
{renderIndicator('平均 ', avgChange)}
|
||
{renderIndicator('最大 ', maxChange)}
|
||
{renderIndicator('周涨 ', weekChange)}
|
||
</Flex>
|
||
);
|
||
};
|
||
|
||
export default StockChangeIndicators;
|