diff --git a/frontend/components/common/admin/files.tsx b/frontend/components/common/admin/files.tsx index cda92b63..d4b365f7 100644 --- a/frontend/components/common/admin/files.tsx +++ b/frontend/components/common/admin/files.tsx @@ -4,38 +4,26 @@ import * as React from "react" import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query" import {AnimatePresence, motion} from "motion/react" import { + Database, Download, Eye, FileArchive, FileAudio, FileImage, + Files, FileText, FileVideo, + FolderOpen, + HardDrive, Loader2, Search, Trash2, + TrendingUp, Upload, X, - HardDrive, - Database, - TrendingUp, - Files, - Info, } from "lucide-react" import {toast} from "sonner" -import { - Area, - AreaChart, - CartesianGrid, - Cell, - Legend, - Pie, - PieChart, - ResponsiveContainer, - Tooltip as RechartsTooltip, - XAxis, - YAxis, -} from "recharts" +import {Area, AreaChart, CartesianGrid, Cell, Pie, PieChart, XAxis, YAxis,} from "recharts" import {Button} from "@/components/ui/button" import {Input} from "@/components/ui/input" @@ -55,6 +43,14 @@ import { import {Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle} from "@/components/ui/sheet" import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card" import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs" +import { + ChartConfig, + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart" import {formatFileSize, getFileUrl, UploadService} from "@/lib/services/upload/upload.service" import type {Upload as UploadRecord} from "@/lib/services/upload/types" @@ -79,14 +75,49 @@ function formatDate(dateStr: string) { }) } -const PIE_COLORS = [ - "#3b82f6", // Blue (图片) - "#10b981", // Emerald (视频) - "#8b5cf6", // Purple (音频) - "#f59e0b", // Amber (文档) - "#ec4899", // Pink (压缩包) - "#64748b", // Slate (其他) -] +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 /* ─── 文件管理主组件 ────────────────────────────────────── */ @@ -100,7 +131,7 @@ export function FilesMain() { const [detailTarget, setDetailTarget] = React.useState(null) const [page, setPage] = React.useState(1) const pageSize = 15 // 表格视图下 15 条更紧凑 - + // 趋势图指标类型切换 (数量 vs 大小) const [trendMetric, setTrendMetric] = React.useState<"count" | "size">("count") @@ -216,38 +247,28 @@ export function FilesMain() { if (!stats?.categories) return [] return stats.categories .filter((c) => c.count > 0) - .map((c) => ({ name: c.name, value: c.count })) + .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) => ({ name: c.name, value: c.size })) - }, [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 } + .map((c) => { + const key = categoryMap[c.name] || "others" + return { + name: key, + value: c.size, + fill: `var(--color-${key})`, + } + }) }, [stats?.categories]) // 近 7 天趋势总新增统计 @@ -271,14 +292,18 @@ export function FilesMain() { > {/* 顶部标题区 */}
-
-

- 文件管理 -

-

- 管理您上传的所有文件,支持下载、数据统计与批量操作 -

+
+
+ +
+
+

文件管理

+

+ 管理您上传的所有文件,支持下载、数据统计与批量操作 +

+
+
@@ -378,20 +403,16 @@ export function FilesMain() {
- + - - - + + + + + + + @@ -409,38 +430,42 @@ export function FilesMain() { style={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} tickFormatter={(v) => (trendMetric === "size" ? formatFileSize(v) : v.toString())} /> - { - if (active && payload && payload.length) { - const data = payload[0] - return ( -
-

{data.payload.date}

-
- - - {trendMetric === "count" ? "新增文件数: " : "新增大小: "} - - {trendMetric === "count" ? `${data.value} 个` : formatFileSize(data.value as number)} - + { + const label = trendMetric === "count" ? "新增文件数" : "新增大小" + const formattedValue = trendMetric === "size" ? formatFileSize(Number(value)) : `${value} 个` + return ( + <> +
+ + {label} +
+ + {formattedValue} -
-
- ) - } - return null - }} + + ) + }} + /> + } />
-
+
@@ -458,7 +483,7 @@ export function FilesMain() {
暂无分类数据
) : (
- + {categoryCountData.map((entry, index) => ( - + ))} - { - if (active && payload && payload.length) { - const data = payload[0].payload - return ( -
-
- - {data.name} -
-
- {data.value} 个 -
-
- ) - } - return null - }} + { + const configObj = statsChartConfig[name as keyof typeof statsChartConfig] + const label = configObj?.label || name + const color = configObj?.color || "hsl(var(--muted))" + return ( + <> +
+ + {label} +
+ + {value} 个 + + + ) + }} + /> + } /> - { - if (!payload) return null - return ( -
- {payload.map((entry: { value: string; color?: string }) => ( -
- - {entry.value} -
- ))} -
- ) - }} + } + className="flex-wrap justify-center gap-x-4 gap-y-1 text-[11px] pt-4" />
-
+
)} -
- -

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

-
@@ -533,7 +544,7 @@ export function FilesMain() {
暂无容量数据
) : (
- + {categorySizeData.map((entry, index) => ( - + ))} - { - if (active && payload && payload.length) { - const data = payload[0].payload - return ( -
-
- - {data.name} -
-
- {formatFileSize(data.value)} -
-
- ) - } - return null - }} + { + 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))} + + + ) + }} + /> + } /> - { - if (!payload) return null - return ( -
- {payload.map((entry: { value: string; color?: string }) => ( -
- - {entry.value} -
- ))} -
- ) - }} + } + className="flex-wrap justify-center gap-x-4 gap-y-1 text-[11px] pt-4" />
-
+
)} -
- -

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

-