- 新增: 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)
118 lines
3.9 KiB
TypeScript
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>
|
|
);
|
|
}
|