'use client'; import * as React from 'react'; import { useTranslations } from 'next-intl'; import { useQuery } from '@tanstack/react-query'; import { Database, Files, HardDrive, Info, Loader2, TrendingUp, Upload, } from 'lucide-react'; import { Area, AreaChart, CartesianGrid, Cell, Pie, PieChart, XAxis, YAxis, } from 'recharts'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, } from '@/components/ui/chart'; import services, { formatFileSize } from '@/lib/services'; const categoryMap: Record = { 图片: 'images', 视频: 'videos', 音频: 'audio', 文档: 'documents', 压缩包: 'archives', 其他: 'others', }; function useStatsChartConfig() { const t = useTranslations('admin.files'); return { images: { label: t('stats.catImages'), color: 'hsl(var(--chart-1))', }, videos: { label: t('stats.catVideos'), color: 'hsl(var(--chart-2))', }, audio: { label: t('stats.catAudio'), color: 'hsl(var(--chart-3))', }, documents: { label: t('stats.catDocuments'), color: 'hsl(var(--chart-4))', }, archives: { label: t('stats.catArchives'), color: 'hsl(var(--chart-5))', }, others: { label: t('stats.catOthers'), color: 'hsl(var(--chart-6))', }, count: { label: t('stats.chartLabelCount'), color: 'hsl(var(--primary))', }, size: { label: t('stats.chartLabelSize'), color: 'hsl(var(--chart-2))', }, } satisfies ChartConfig; } export function FileStats() { const t = useTranslations('admin.files'); const statsChartConfig = useStatsChartConfig(); const [trendMetric, setTrendMetric] = React.useState<'count' | 'size'>( 'count', ); const statsQuery = useQuery({ queryKey: ['files', 'stats'], queryFn: () => services.adminUpload.getFileStats(), staleTime: 0, refetchOnMount: 'always', }); const stats = statsQuery.data; const trendData = React.useMemo(() => { if (!stats?.trend) return []; return stats.trend.map((item) => ({ ...item, formattedDate: item.date.substring(5), // YYYY-MM-DD -> MM-DD value: trendMetric === 'count' ? item.count : item.size, })); }, [stats?.trend, trendMetric]); const categoryCountData = React.useMemo(() => { if (!stats?.categories) return []; return stats.categories .filter((c) => c.count > 0) .map((c) => { const key = categoryMap[c.name] || 'others'; return { name: key, value: c.count, fill: `var(--color-${key})`, }; }); }, [stats?.categories]); const categorySizeData = React.useMemo(() => { if (!stats?.categories) return []; return stats.categories .filter((c) => c.size > 0) .map((c) => { const key = categoryMap[c.name] || 'others'; return { name: key, value: c.size, fill: `var(--color-${key})`, }; }); }, [stats?.categories]); const maxStats = React.useMemo(() => { if (!stats?.categories || stats.categories.length === 0) { return { maxCountName: t('stats.none'), maxCount: 0, maxSizeName: t('stats.none'), maxSize: 0, }; } let maxCountName = t('stats.none'); let maxCount = 0; let maxSizeName = t('stats.none'); let maxSize = 0; stats.categories.forEach((cat) => { if (cat.count > maxCount) { maxCount = cat.count; maxCountName = cat.name; } if (cat.size > maxSize) { maxSize = cat.size; maxSizeName = cat.name; } }); return { maxCountName, maxCount, maxSizeName, maxSize }; }, [stats?.categories, t]); const trendSummary = React.useMemo(() => { if (!stats?.trend) return { count: 0, size: 0 }; return stats.trend.reduce( (acc, curr) => ({ count: acc.count + curr.count, size: acc.size + curr.size, }), { count: 0, size: 0 }, ); }, [stats?.trend]); if (statsQuery.isLoading) { return (
); } if (!stats || stats.total_count === 0) { return (

{t('stats.noData')}

{t('stats.noDataDesc')}

); } return (
{/* 四个指标卡片 */}
{t('stats.totalFiles')}
{stats.total_count.toLocaleString()}{' '} {t('stats.countUnit')}

{t('stats.totalFilesDesc')}

{t('stats.totalSize')}
{formatFileSize(stats.total_size)}

{t('stats.totalSizeDesc')}

{t('stats.weeklyNewFiles')}
+{trendSummary.count.toLocaleString()}{' '} {t('stats.countUnit')}

{t('stats.weeklyNewFilesDesc')}

{t('stats.weeklyNewSize')}
+{formatFileSize(trendSummary.size)}

{t('stats.weeklyNewSizeDesc')}

{/* 7 天新增趋势折线图 */}
{t('stats.trendTitle')} {t('stats.trendDesc')}
trendMetric === 'size' ? formatFileSize(v) : v.toString() } /> { const label = trendMetric === 'count' ? t('stats.chartLabelCount') : t('stats.chartLabelSize'); const formattedValue = trendMetric === 'size' ? formatFileSize(Number(value)) : `${value} ${t('stats.countUnit')}`; return ( <>
{label}
{formattedValue} ); }} /> } />
{/* 两个饼图分布 */}
{/* 饼图 1: 格式数量分布 */} {t('stats.countDistTitle')} {t('stats.countDistDesc')} {categoryCountData.length === 0 ? (
{t('stats.noCategoryData')}
) : (
{categoryCountData.map((entry, index) => ( ))} { const configObj = statsChartConfig[ name as keyof typeof statsChartConfig ]; const label = configObj?.label || name; const color = configObj?.color || 'hsl(var(--muted))'; return ( <>
{label}
{value} {t('stats.countUnit')} ); }} /> } /> } />
)}

{t('stats.maxCountHint', { name: maxStats.maxCountName, count: maxStats.maxCount, })}

{/* 饼图 2: 格式容量分布 */} {t('stats.sizeDistTitle')} {t('stats.sizeDistDesc')} {categorySizeData.length === 0 ? (
{t('stats.noSizeData')}
) : (
{categorySizeData.map((entry, index) => ( ))} { const configObj = statsChartConfig[ name as keyof typeof statsChartConfig ]; const label = configObj?.label || name; const color = configObj?.color || 'hsl(var(--muted))'; return ( <>
{label}
{formatFileSize(Number(value))} ); }} /> } /> } />
)}

{t('stats.maxSizeHint', { name: maxStats.maxSizeName, size: formatFileSize(maxStats.maxSize), })}

); }