Files
pixel/frontend/src/components/admin/common/PageStats.tsx
张鹏 f9f4560459 Initial commit: Pixel AI comic/video creation platform
- FastAPI backend with SQLModel, Alembic migrations, AgentScope agents
- Next.js 15 frontend with React 19, Tailwind, Zustand, React Flow
- Multi-provider AI system (DashScope, Kling, MiniMax, Volcengine, OpenAI, etc.)
- All HTTP clients migrated from sync requests to async httpx
- Admin-managed API keys via environment variables
- SSRF vulnerability fixed in ensure_url()
2026-04-29 01:20:12 +08:00

82 lines
2.1 KiB
TypeScript

'use client';
import { ReactNode } from 'react';
import { cn } from '@/lib/utils';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
interface PageStatsProps {
children: ReactNode;
className?: string;
columns?: 2 | 3 | 4 | 5 | 6;
}
export function PageStats({ children, className, columns = 4 }: PageStatsProps) {
const columnClasses = {
2: 'grid-cols-1 sm:grid-cols-2',
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
5: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-5',
6: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6',
};
return (
<div className={cn('grid gap-4', columnClasses[columns], className)}>
{children}
</div>
);
}
interface PageStatCardProps {
title: string;
value: string | number;
icon?: ReactNode;
description?: string;
loading?: boolean;
trend?: {
value: number;
label: string;
positive?: boolean;
};
className?: string;
}
export function PageStatCard({
title,
value,
icon,
description,
loading,
trend,
className,
}: PageStatCardProps) {
return (
<Card className={className}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
{icon && <div className="text-muted-foreground">{icon}</div>}
</CardHeader>
<CardContent>
{loading ? (
<Skeleton className="h-8 w-24" />
) : (
<div className="text-2xl font-bold">{value}</div>
)}
{description && (
<p className="text-xs text-muted-foreground mt-1">{description}</p>
)}
{trend && (
<p className="text-xs mt-1">
<span className={trend.positive ? 'text-green-500' : 'text-red-500'}>
{trend.positive ? '↑' : '↓'} {Math.abs(trend.value)}%
</span>
{' '}{trend.label}
</p>
)}
</CardContent>
</Card>
);
}
export default PageStats;