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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
position="relative"
|
position="relative"
|
||||||
bgGradient={themeColors.bgGradient}
|
|
||||||
color="white"
|
color="white"
|
||||||
overflow="hidden"
|
|
||||||
zIndex={1}
|
zIndex={1}
|
||||||
mx={fullWidthMargin}
|
mx={fullWidthMargin}
|
||||||
borderRadius={fullWidth ? undefined : 'xl'}
|
borderRadius={fullWidth ? undefined : 'xl'}
|
||||||
borderBottom={`1px solid ${themeColors.borderColor}`}
|
|
||||||
{...boxProps}
|
{...boxProps}
|
||||||
>
|
>
|
||||||
{/* 背景装饰层 */}
|
{/* 背景容器 - 处理渐变和过渡 */}
|
||||||
<HeroBackground
|
<Box
|
||||||
decorations={decorations}
|
position="absolute"
|
||||||
themeColors={themeColors}
|
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 */}
|
{/* 主内容区 - 自适应高度,上下 padding 40px */}
|
||||||
<Box
|
<Box
|
||||||
px={{ base: 4, md: 6, lg: fullWidth ? '80px' : 6 }}
|
px={{ base: 4, md: 6, lg: fullWidth ? '80px' : 6 }}
|
||||||
py="40px"
|
py="40px"
|
||||||
|
pb="60px"
|
||||||
display="flex"
|
display="flex"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
position="relative"
|
position="relative"
|
||||||
|
zIndex={2}
|
||||||
>
|
>
|
||||||
{children ? (
|
{children ? (
|
||||||
// 完全自定义内容
|
// 完全自定义内容
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* StatCard 单个统计卡片
|
* StatCard 单个统计卡片
|
||||||
|
* 支持趋势变化显示和多空进度条
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
@@ -12,15 +13,138 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
Text,
|
Text,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
} from '@chakra-ui/react';
|
} 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>(({
|
export const StatCard = memo<StatCardProps>(({
|
||||||
item,
|
item,
|
||||||
themeColors,
|
themeColors,
|
||||||
showIcon = false,
|
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 = (() => {
|
const displayValue = (() => {
|
||||||
@@ -67,24 +191,50 @@ export const StatCard = memo<StatCardProps>(({
|
|||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
border="1px solid"
|
border="1px solid"
|
||||||
borderColor={themeColors.statCardBorder}
|
borderColor={themeColors.statCardBorder}
|
||||||
|
position="relative"
|
||||||
|
overflow="hidden"
|
||||||
>
|
>
|
||||||
<StatLabel
|
{/* 水印背景图标 */}
|
||||||
color={themeColors.statLabelColor}
|
{watermark && <WatermarkIcon config={watermark} />}
|
||||||
fontWeight="medium"
|
|
||||||
fontSize="xs"
|
<Box position="relative" zIndex={1}>
|
||||||
>
|
<StatLabel
|
||||||
{label}
|
color={themeColors.statLabelColor}
|
||||||
</StatLabel>
|
fontWeight="medium"
|
||||||
{isLoading ? (
|
fontSize="xs"
|
||||||
<Skeleton height="24px" width="60px" mx="auto" mt={1} />
|
|
||||||
) : (
|
|
||||||
<StatNumber
|
|
||||||
fontSize="lg"
|
|
||||||
color={valueColor || 'white'}
|
|
||||||
>
|
>
|
||||||
{displayValue}
|
{label}
|
||||||
</StatNumber>
|
</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>
|
</Stat>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user