未命名 3
// components/MediaUpload.tsx
"use client";
import { Loader2, Upload, X } from "lucide-react";
import * as React from "react";
import { toast } from "sonner";
import { Button } from "./ui/button";
import { Card } from "./ui/card";
import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useBatchUploadMedia } from "@/hooks/api";
import { Upload } from "@/components/ui/upload";
const CATEGORY_OPTIONS = [
{ value: "product", label: "商品图片" },
{ value: "hero_card", label: "爆款卡片" },
{ value: "ad", label: "广告图片" },
{ value: "document", label: "文档资料" },
{ value: "general", label: "通用文件" },
];
export function MediaUpload({
children,
onUploadComplete,
}: {
children: React.ReactNode;
onUploadComplete?: () => void;
}) {
const [open, setOpen] = React.useState(false);
const [category, setCategory] = React.useState<string>("general");
const { mutateAsync, isPending } = useBatchUploadMedia();
const [selectedFiles, setSelectedFiles] = React.useState<File[]>([]);
const [previews, setPreviews] = React.useState<Record<number, string>>({});
const fileInputRef = React.useRef<HTMLInputElement>(null);
// 生成图片预览
React.useEffect(() => {
selectedFiles.forEach((file, index) => {
if (file.type.startsWith("image/") && !previews[index]) {
const reader = new FileReader();
reader.onload = (e) => {
setPreviews((prev) => ({
...prev,
[index]: e.target?.result as string,
}));
};
reader.readAsDataURL(file);
}
});
// 清理不再需要的预览
return () => {
Object.keys(previews).forEach((key) => {
if (parseInt(key) >= selectedFiles.length) {
setPreviews((prev) => {
const newPreviews = { ...prev };
delete newPreviews[parseInt(key)];
return newPreviews;
});
}
});
};
}, [selectedFiles]);
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && files.length > 0) {
const fileList = Array.from(files);
console.log("选择的文件:", fileList);
setSelectedFiles((prev) => {
const newFiles = [...prev, ...fileList];
console.log("更新后的文件列表:", newFiles);
return newFiles;
});
// 重置 input value,允许重复选择同一个文件
e.target.value = "";
}
};
const handleRemoveFile = (index: number) => {
setSelectedFiles((prev) => prev.filter((_, i) => i !== index));
// 清理对应的预览
setPreviews((prev) => {
const newPreviews = { ...prev };
delete newPreviews[index];
// 重新索引剩余的预览
const updatedPreviews: Record<number, string> = {};
Object.keys(newPreviews).forEach((key) => {
const keyNum = parseInt(key);
if (keyNum > index) {
updatedPreviews[keyNum - 1] = newPreviews[keyNum];
} else {
updatedPreviews[keyNum] = newPreviews[keyNum];
}
});
return updatedPreviews;
});
};
// 处理文件上传(一次性上传所有文件)
const handleBatchUpload = async () => {
if (selectedFiles.length === 0) return;
try {
console.log(
"开始批量上传文件数量:",
selectedFiles.length,
"分类:",
category
);
// 一次性上传所有文件,直接传递 File 数组和 category
const result = await mutateAsync({
files: selectedFiles,
category,
});
console.log("所有文件上传成功:", result);
toast.success(成功上传 ${selectedFiles.length} 个文件!);
setOpen(false); // 上传成功自动关窗
setCategory("general"); // 重置分类选择
setSelectedFiles([]); // 清空文件列表
onUploadComplete?.(); // 刷新列表数据
} catch (error) {
console.error("批量上传失败:", error);
const errorMessage = error instanceof Error ? error.message : "上传失败";
toast.error(errorMessage);
}
};
// 格式化文件大小
const formatFileSize = (bytes: number) => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return ${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]};
};
return (
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-h-[90vh] max-w-3xl overflow-y-auto sm:rounded-2xl">
<DialogHeader>
<DialogTitle className="font-bold text-xl">上传媒体资源</DialogTitle>
<DialogDescription>
选择分类后,点击选择文件按钮或拖拽文件到上传区域
</DialogDescription>
</DialogHeader>
{/* 分类选择 */}
<div className="space-y-2">
<Label htmlFor="category-select">文件分类 *</Label>
<Select
defaultValue="general"
onValueChange={(value) => setCategory(value)}
value={category}
>
<SelectTrigger className="w-full" id="category-select">
<SelectValue placeholder="选择文件分类" />
</SelectTrigger>
<SelectContent>
{CATEGORY_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 文件选择区域 */}
<div
className="flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-slate-300 border-dashed bg-slate-50 p-8 text-center transition-colors hover:border-indigo-400 hover:bg-indigo-50"
onClick={() => fileInputRef.current?.click()}
>
<Upload className="mb-4 size-10 text-slate-400" />
<p className="font-medium text-slate-700 text-sm">
点击选择文件或拖拽文件到此处
</p>
<p className="text-slate-500 text-xs">
支持图片、视频、PDF 等格式,单个文件最大 10MB
</p>
<input
ref={fileInputRef}
type="file"
multiple
accept="image/,video/,.pdf,.doc,.docx"
className="hidden"
onChange={handleFileSelect}
/>
</div>
{/* 已选择的文件列表 */}
{selectedFiles.length > 0 && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="font-medium text-sm">
已选择 {selectedFiles.length} 个文件
</h4>
<button
className="text-slate-500 text-xs hover:text-slate-700"
onClick={() => setSelectedFiles([])}
>
清空全部
</button>
</div>
<div className="space-y-2">
{selectedFiles.map((file, index) => (
<Card
className="flex items-center justify-between p-3"
key={${file.name}-${index}}
>
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-sm">{file.name}</p>
<p className="text-slate-500 text-xs">
{formatFileSize(file.size)} • {file.type || "未知类型"}
</p>
</div>
<Button
className="h-8 w-8 shrink-0"
onClick={() => handleRemoveFile(index)}
size="icon"
variant="ghost"
>
<X className="size-4" />
</Button>
</Card>
))}
</div>
</div>
)}
{/* 上传按钮 */}
{selectedFiles.length > 0 && (
<div className="flex justify-end border-t pt-4">
<Button
className="bg-indigo-600 hover:bg-indigo-700"
disabled={isPending}
onClick={handleBatchUpload}
>
{isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
上传中...
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
开始上传 ({selectedFiles.length})
</>
)}
</Button>
</div>
)}
{isPending && (
<div className="flex items-center justify-center gap-2 pb-4 font-medium text-indigo-600 text-sm">
<Loader2 className="h-4 w-4 animate-spin" />
正在同步到服务器...
</div>
)}
</DialogContent>
</Dialog>
);
}