- 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()
82 lines
2.1 KiB
TypeScript
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;
|