'use client'; import * as React from 'react'; 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', }; const statsChartConfig = { images: { label: '图片', color: 'hsl(var(--chart-1))', }, videos: { label: '视频', color: 'hsl(var(--chart-2))', }, audio: { label: '音频', color: 'hsl(var(--chart-3))', }, documents: { label: '文档', color: 'hsl(var(--chart-4))', }, archives: { label: '压缩包', color: 'hsl(var(--chart-5))', }, others: { label: '其他', color: 'hsl(var(--chart-6))', }, count: { label: '新增文件数', color: 'hsl(var(--primary))', }, size: { label: '新增大小', color: 'hsl(var(--chart-2))', }, } satisfies ChartConfig; export function FileStats() { 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: '无', maxCount: 0, maxSizeName: '无', maxSize: 0 }; } let maxCountName = '无'; let maxCount = 0; let maxSizeName = '无'; 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]); 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 (

暂无上传数据

上传文件后即可在此查看容量与文件分布分析。

); } return (
{/* 四个指标卡片 */}
总文件数
{stats.total_count.toLocaleString()}{' '} 个

您上传的可用文件数量汇总

占用存储容量
{formatFileSize(stats.total_size)}

文件实际在磁盘/S3中占用的总空间

近 7 天新增文件
+{trendSummary.count.toLocaleString()}{' '} 个

最近 7 天上传成功的文件总数

近 7 天新增大小
+{formatFileSize(trendSummary.size)}

最近 7 天上传所消耗的存储带宽

{/* 7 天新增趋势折线图 */}
最近 7 天新增趋势 按日统计近一周期内的上传波动
trendMetric === 'size' ? formatFileSize(v) : v.toString() } /> { const label = trendMetric === 'count' ? '新增文件数' : '新增大小'; const formattedValue = trendMetric === 'size' ? formatFileSize(Number(value)) : `${value} 个`; return ( <>
{label}
{formattedValue} ); }} /> } />
{/* 两个饼图分布 */}
{/* 饼图 1: 格式数量分布 */} 文件格式数量分布 各种格式类型的文件个数占比 {categoryCountData.length === 0 ? (
暂无分类数据
) : (
{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} 个 ); }} /> } /> } className='flex-wrap justify-center gap-x-4 gap-y-1 text-[11px] pt-4' />
)}

文件类型中数量最多的是{' '} 「{maxStats.maxCountName}」 ,累计上传了{' '} {maxStats.maxCount} {' '} 个文件。

{/* 饼图 2: 格式容量分布 */} 文件占用容量分布 各种格式类型的文件大小占比 {categorySizeData.length === 0 ? (
暂无容量数据
) : (
{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))} ); }} /> } /> } className='flex-wrap justify-center gap-x-4 gap-y-1 text-[11px] pt-4' />
)}

文件类型中占用存储最大的是{' '} 「{maxStats.maxSizeName}」 ,共消耗了{' '} {formatFileSize(maxStats.maxSize)} {' '} 存储容量。

); }