"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)}{" "} 存储容量。

) }