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:
zdl
2025-12-30 15:55:18 +08:00
parent d549eaaf9f
commit 9deb9ff350
2 changed files with 195 additions and 25 deletions

View File

@@ -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 ? (
// 完全自定义内容

View File

@@ -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>
);
});