返回首页

未命名 3

分类:shop项目
发布于:
阅读时间:46 分钟

// 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>

  );

}