Files
aiops/frontend/src/components/dashboard/DashboardStats.tsx
T
attacker 722587ab84 feat: 前端全面优化 — 懒加载/骨架屏/错误边界/面包屑/主题增强/Glass登录页/动画系统
- 新增: ErrorBoundary, PageTransition, Breadcrumb, SkeletonCard (4组件)
- 重构: App.tsx (React.lazy懒加载所有页面), index.css (55+主题变量/动画/工具类)
- 优化: MainLayout (CSS变量布局+Footer), Header (搜索增强+快捷键), Sidebar (折叠Tooltip+子菜单动画)
- 优化: Dashboard骨架屏+级联动画, Stats/Charts/Recent主题感知颜色
- 优化: Login.tsx Glass毛玻璃效果+密码显隐图标+光球动画
- 图表Recharts主题化(CustomTooltip/chart-grid/chart-axis)
- 减少动画偏好支持(prefers-reduced-motion)
2026-06-24 02:06:07 +00:00

118 lines
3.9 KiB
TypeScript

import { Card, CardContent } from '@/components/ui/card';
import { TrendingUp, TrendingDown, Minus } from 'lucide-react';
import { cn } from '@/lib/utils';
/**
* StatCard - 优化后的统计卡片组件
*
* 优化点:
* 1. 使用 dg-card 工具类替代硬编码样式
* 2. 添加 hover 效果和点击交互
* 3. 图标颜色通过主题变量映射
* 4. 趋势显示更直观
* 5. 添加 shimmer loading 效果支持
*/
const colorClasses: Record<string, { bg: string; icon: string; glow: string }> = {
blue: { bg: 'bg-[var(--dg-blue)]/10', icon: 'text-[var(--dg-blue)]', glow: 'shadow-[var(--dg-blue)]/10' },
emerald: { bg: 'bg-[var(--dg-green)]/10', icon: 'text-[var(--dg-green)]', glow: 'shadow-[var(--dg-green)]/10' },
amber: { bg: 'bg-[var(--dg-yellow)]/10', icon: 'text-[var(--dg-yellow)]', glow: 'shadow-[var(--dg-yellow)]/10' },
rose: { bg: 'bg-[var(--dg-red)]/10', icon: 'text-[var(--dg-red)]', glow: 'shadow-[var(--dg-red)]/10' },
purple: { bg: 'bg-[var(--dg-purple)]/10', icon: 'text-[var(--dg-purple)]', glow: 'shadow-[var(--dg-purple)]/10' },
cyan: { bg: 'bg-[var(--dg-cyan)]/10', icon: 'text-[var(--dg-cyan)]', glow: 'shadow-[var(--dg-cyan)]/10' },
orange: { bg: 'bg-[var(--dg-orange)]/10', icon: 'text-[var(--dg-orange)]', glow: 'shadow-[var(--dg-orange)]/10' },
};
interface StatCardProps {
title: string;
value: string | number;
description?: string;
icon: React.ElementType;
trend?: 'up' | 'down' | 'neutral';
trendValue?: string;
color: string;
onClick?: () => void;
loading?: boolean;
}
export function StatCard({
title,
value,
description,
icon: Icon,
trend,
trendValue,
color,
onClick,
loading = false,
}: StatCardProps) {
const colors = colorClasses[color] || colorClasses.blue;
if (loading) {
return (
<Card className="dg-card overflow-hidden">
<CardContent className="p-6 space-y-4">
<div className="flex items-start justify-between">
<div className="space-y-3 flex-1">
<div className="dg-skeleton h-4 w-24 rounded" />
<div className="dg-skeleton h-8 w-16 rounded" />
</div>
<div className="dg-skeleton h-10 w-10 rounded-lg" />
</div>
<div className="dg-skeleton h-3 w-32 rounded" />
</CardContent>
</Card>
);
}
const TrendIcon = trend === 'up' ? TrendingUp : trend === 'down' ? TrendingDown : Minus;
const trendColor = trend === 'up'
? 'text-[var(--dg-green)]'
: trend === 'down'
? 'text-[var(--dg-red)]'
: 'text-[var(--dg-text-muted)]';
return (
<Card
className={cn(
'dg-card dg-card-hover cursor-pointer overflow-hidden',
onClick && 'cursor-pointer',
)}
onClick={onClick}
>
<CardContent className="p-6">
<div className="flex items-start justify-between gap-4">
<div className="space-y-3 min-w-0 flex-1">
<p className="text-sm font-medium text-[var(--dg-text-secondary)] truncate">
{title}
</p>
<div className="flex items-baseline gap-2 flex-wrap">
<h3 className="text-2xl font-bold text-[var(--dg-text-primary)] tracking-tight">
{value}
</h3>
{trend && trendValue && (
<span className={cn('flex items-center text-xs font-semibold gap-0.5', trendColor)}>
<TrendIcon className="h-3.5 w-3.5" />
{trendValue}
</span>
)}
</div>
{description && (
<p className="text-xs text-[var(--dg-text-muted)]">{description}</p>
)}
</div>
<div
className={cn(
'p-3 rounded-xl flex-shrink-0',
colors.bg,
colors.icon,
)}
>
<Icon className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
);
}