feat(StatCard): 新增趋势指示器和多空进度条组件
- 新增 TrendIndicator 组件显示环比变化(箭头+百分比+标签) - 新增 BullBearBar 组件显示红绿进度条 - 新增 WatermarkIcon 组件支持卡片水印背景 - 支持双色数值显示(如 121/79 红绿分色) - StatCard 根据配置自动渲染趋势和进度条 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -52,29 +52,49 @@ export const HeroSection = memo<HeroSectionProps>(({
|
||||
return (
|
||||
<Box
|
||||
position="relative"
|
||||
bgGradient={themeColors.bgGradient}
|
||||
color="white"
|
||||
overflow="hidden"
|
||||
zIndex={1}
|
||||
mx={fullWidthMargin}
|
||||
borderRadius={fullWidth ? undefined : 'xl'}
|
||||
borderBottom={`1px solid ${themeColors.borderColor}`}
|
||||
{...boxProps}
|
||||
>
|
||||
{/* 背景装饰层 */}
|
||||
<HeroBackground
|
||||
decorations={decorations}
|
||||
themeColors={themeColors}
|
||||
{/* 背景容器 - 处理渐变和过渡 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
inset={0}
|
||||
bgGradient={themeColors.bgGradient}
|
||||
overflow="hidden"
|
||||
zIndex={0}
|
||||
>
|
||||
{/* 背景装饰层 */}
|
||||
<HeroBackground
|
||||
decorations={decorations}
|
||||
themeColors={themeColors}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 底部渐变过渡区域 - 创造纵深感 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom={0}
|
||||
left={0}
|
||||
right={0}
|
||||
h="80px"
|
||||
bgGradient="linear(to-b, transparent, rgba(10, 10, 15, 0.8))"
|
||||
pointerEvents="none"
|
||||
zIndex={1}
|
||||
/>
|
||||
|
||||
{/* 主内容区 - 自适应高度,上下 padding 40px */}
|
||||
<Box
|
||||
px={{ base: 4, md: 6, lg: fullWidth ? '80px' : 6 }}
|
||||
py="40px"
|
||||
pb="60px"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
position="relative"
|
||||
zIndex={2}
|
||||
>
|
||||
{children ? (
|
||||
// 完全自定义内容
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* StatCard 单个统计卡片
|
||||
* 支持趋势变化显示和多空进度条
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
@@ -12,15 +13,138 @@ import {
|
||||
Icon,
|
||||
Text,
|
||||
Skeleton,
|
||||
Box,
|
||||
Flex,
|
||||
} from '@chakra-ui/react';
|
||||
import type { StatCardProps } from '../../types';
|
||||
import { ArrowUp, ArrowDown, Minus } from 'lucide-react';
|
||||
import type { StatCardProps, TrendInfo, ProgressBarConfig, WatermarkIconConfig } from '../../types';
|
||||
|
||||
/** 趋势指示器组件 */
|
||||
const TrendIndicator = memo<{ trend: TrendInfo }>(({ trend }) => {
|
||||
const { direction, percent, label, compareText = '较昨日' } = trend;
|
||||
|
||||
// 根据方向确定颜色和图标
|
||||
const colorMap = {
|
||||
up: '#ff4d4d',
|
||||
down: '#22c55e',
|
||||
flat: 'gray.400',
|
||||
};
|
||||
const IconMap = {
|
||||
up: ArrowUp,
|
||||
down: ArrowDown,
|
||||
flat: Minus,
|
||||
};
|
||||
const color = colorMap[direction];
|
||||
const TrendIcon = IconMap[direction];
|
||||
|
||||
// 格式化百分比
|
||||
const formattedPercent = direction === 'flat' ? '0%' : `${direction === 'up' ? '+' : ''}${percent.toFixed(1)}%`;
|
||||
|
||||
return (
|
||||
<HStack spacing={1} justify="center" mt={1}>
|
||||
<Icon as={TrendIcon} boxSize={3} color={color} />
|
||||
<Text fontSize="xs" color={color} fontWeight="medium">
|
||||
{formattedPercent}
|
||||
</Text>
|
||||
{label && (
|
||||
<Text fontSize="xs" color={color} opacity={0.8}>
|
||||
({label})
|
||||
</Text>
|
||||
)}
|
||||
<Text fontSize="xs" color="whiteAlpha.500">
|
||||
{compareText}
|
||||
</Text>
|
||||
</HStack>
|
||||
);
|
||||
});
|
||||
|
||||
TrendIndicator.displayName = 'TrendIndicator';
|
||||
|
||||
/** 多空进度条组件 */
|
||||
const BullBearBar = memo<{ config: ProgressBarConfig }>(({ config }) => {
|
||||
const {
|
||||
value,
|
||||
total,
|
||||
positiveColor = '#ff4d4d',
|
||||
negativeColor = '#22c55e',
|
||||
compareLabel,
|
||||
compareValue,
|
||||
} = config;
|
||||
|
||||
// 计算百分比
|
||||
const sum = value + total;
|
||||
const positivePercent = sum > 0 ? (value / sum) * 100 : 50;
|
||||
const negativePercent = 100 - positivePercent;
|
||||
|
||||
return (
|
||||
<VStack spacing={1} w="100%" mt={2}>
|
||||
{/* 进度条 */}
|
||||
<Box
|
||||
w="100%"
|
||||
h="6px"
|
||||
borderRadius="full"
|
||||
overflow="hidden"
|
||||
bg="whiteAlpha.200"
|
||||
>
|
||||
<Flex h="100%">
|
||||
<Box
|
||||
w={`${positivePercent}%`}
|
||||
bg={positiveColor}
|
||||
borderRadius="full"
|
||||
transition="width 0.3s ease"
|
||||
/>
|
||||
<Box
|
||||
w={`${negativePercent}%`}
|
||||
bg={negativeColor}
|
||||
borderRadius="full"
|
||||
transition="width 0.3s ease"
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
{/* 标签(仅当有 compareLabel 时显示) */}
|
||||
{compareLabel && compareValue !== undefined && (
|
||||
<Flex w="100%" justify="space-between" fontSize="xs">
|
||||
<Text color={positiveColor} fontWeight="medium">
|
||||
{value.toLocaleString()}
|
||||
</Text>
|
||||
<Text color={negativeColor} fontWeight="medium">
|
||||
{compareLabel}: {compareValue.toLocaleString()}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
});
|
||||
|
||||
BullBearBar.displayName = 'BullBearBar';
|
||||
|
||||
/** 水印图标组件 */
|
||||
const WatermarkIcon = memo<{ config: WatermarkIconConfig }>(({ config }) => {
|
||||
const { icon: WIcon, color = 'white', opacity = 0.08, size = 48 } = config;
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="absolute"
|
||||
top="50%"
|
||||
right={3}
|
||||
transform="translateY(-50%)"
|
||||
opacity={opacity}
|
||||
pointerEvents="none"
|
||||
zIndex={0}
|
||||
>
|
||||
<Icon as={WIcon} boxSize={`${size}px`} color={color} />
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
WatermarkIcon.displayName = 'WatermarkIcon';
|
||||
|
||||
export const StatCard = memo<StatCardProps>(({
|
||||
item,
|
||||
themeColors,
|
||||
showIcon = false,
|
||||
}) => {
|
||||
const { label, value, valueColor, icon, iconColor, suffix, isLoading } = item;
|
||||
const { label, value, valueColor, icon, iconColor, suffix, isLoading, trend, progressBar, watermark } = item;
|
||||
|
||||
// 格式化显示值
|
||||
const displayValue = (() => {
|
||||
@@ -67,24 +191,50 @@ export const StatCard = memo<StatCardProps>(({
|
||||
borderRadius="md"
|
||||
border="1px solid"
|
||||
borderColor={themeColors.statCardBorder}
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
>
|
||||
<StatLabel
|
||||
color={themeColors.statLabelColor}
|
||||
fontWeight="medium"
|
||||
fontSize="xs"
|
||||
>
|
||||
{label}
|
||||
</StatLabel>
|
||||
{isLoading ? (
|
||||
<Skeleton height="24px" width="60px" mx="auto" mt={1} />
|
||||
) : (
|
||||
<StatNumber
|
||||
fontSize="lg"
|
||||
color={valueColor || 'white'}
|
||||
{/* 水印背景图标 */}
|
||||
{watermark && <WatermarkIcon config={watermark} />}
|
||||
|
||||
<Box position="relative" zIndex={1}>
|
||||
<StatLabel
|
||||
color={themeColors.statLabelColor}
|
||||
fontWeight="medium"
|
||||
fontSize="xs"
|
||||
>
|
||||
{displayValue}
|
||||
</StatNumber>
|
||||
)}
|
||||
{label}
|
||||
</StatLabel>
|
||||
{isLoading ? (
|
||||
<Skeleton height="24px" width="60px" mx="auto" mt={1} />
|
||||
) : (
|
||||
<>
|
||||
{/* 多空对比特殊渲染:双色显示 */}
|
||||
{progressBar && typeof value === 'string' && value.includes('/') ? (
|
||||
<StatNumber fontSize="lg">
|
||||
<Text as="span" color={progressBar.positiveColor || '#ff4d4d'}>
|
||||
{value.split('/')[0]}
|
||||
</Text>
|
||||
<Text as="span" color="whiteAlpha.500">/</Text>
|
||||
<Text as="span" color={progressBar.negativeColor || '#22c55e'}>
|
||||
{value.split('/')[1]}
|
||||
</Text>
|
||||
</StatNumber>
|
||||
) : (
|
||||
<StatNumber
|
||||
fontSize="lg"
|
||||
color={valueColor || 'white'}
|
||||
>
|
||||
{displayValue}
|
||||
</StatNumber>
|
||||
)}
|
||||
{/* 趋势指示器 */}
|
||||
{trend && <TrendIndicator trend={trend} />}
|
||||
{/* 多空进度条 */}
|
||||
{progressBar && <BullBearBar config={progressBar} />}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Stat>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user