fileDragHandler.ts 7.67 KB
// 支持的文件类型
const SUPPORTED_FILE_TYPES: Record<string, { icon: string; name: string }> = {
  "application/pdf": { icon: "fas fa-file-pdf", name: "PDF文档" },
  "application/msword": { icon: "fas fa-file-word", name: "Word文档" },
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document": {
    icon: "fas fa-file-word",
    name: "Word文档",
  },
  "application/vnd.ms-excel": { icon: "fas fa-file-excel", name: "Excel文档" },
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {
    icon: "fas fa-file-excel",
    name: "Excel文档",
  },
  "application/vnd.ms-powerpoint": { icon: "fas fa-file-powerpoint", name: "PowerPoint文档" },
  "application/vnd.openxmlformats-officedocument.presentationml.presentation": {
    icon: "fas fa-file-powerpoint",
    name: "PowerPoint文档",
  },
  "text/plain": { icon: "fas fa-file-alt", name: "文本文件" },
  "text/markdown": { icon: "fas fa-file-alt", name: "Markdown文件" },
  "image/jpeg": { icon: "fas fa-file-image", name: "JPEG图片" },
  "image/png": { icon: "fas fa-file-image", name: "PNG图片" },
  "image/gif": { icon: "fas fa-file-image", name: "GIF图片" },
  "application/json": { icon: "fas fa-file-code", name: "JSON文件" },
};

// 支持的文件扩展名映射到 MIME 类型
const SUPPORTED_EXTENSIONS: Record<string, string> = {
  pdf: "application/pdf",
  doc: "application/msword",
  docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  xls: "application/vnd.ms-excel",
  xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  ppt: "application/vnd.ms-powerpoint",
  pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
  md: "text/markdown",
  markdown: "text/markdown",
  txt: "text/plain",
  jpg: "image/jpeg",
  jpeg: "image/jpeg",
  png: "image/png",
  gif: "image/gif",
  json: "application/json",
};

// 获取文件扩展名
const getFileExtension = (fileName: string): string | null => {
  if (!fileName || fileName.indexOf(".") === -1) {
    return null;
  }
  const parts = fileName.split(".");
  const ext = parts[parts.length - 1];
  return ext ? ext.toLowerCase() : null;
};

// 文件验证
export const validateFile = (
  file: File,
  maxSize?: number, // 最大文件大小(字节),默认 100MB
): { isValid: boolean; errors: string[] } => {
  const errors: string[] = [];

  if (!file.name || file.name.trim() === "") {
    errors.push("文件名不能为空");
    return {
      isValid: false,
      errors,
    };
  }

  // 检查文件大小(默认 100MB)
  const maxFileSize = maxSize ?? 100 * 1024 * 1024;
  if (file.size > maxFileSize) {
    const maxSizeMB = (maxFileSize / (1024 * 1024)).toFixed(0);
    errors.push(`文件大小不能超过 ${maxSizeMB}MB`);
  }

  // 首先检查 MIME 类型
  let isValidType = !!SUPPORTED_FILE_TYPES[file.type];
  
  // 如果 MIME 类型不匹配,尝试通过文件扩展名验证
  if (!isValidType) {
    const ext = getFileExtension(file.name);
    if (ext && SUPPORTED_EXTENSIONS[ext]) {
      // 通过扩展名找到了支持的类型
      isValidType = true;
    } else {
      errors.push(`不支持的文件类型: ${file.type || "未知类型"}${ext ? ` (扩展名: .${ext})` : ""}`);
    }
  }

  return {
    isValid: isValidType && errors.length === 0,
    errors,
  };
};

// 文件扩展名到 SVG 图标路径的映射
const FILE_EXTENSION_TO_ICON: Record<string, string> = {
  pdf: "/pdf_icon.svg",
  doc: "/word_icon.svg",
  docx: "/word_icon.svg",
  xls: "/excel_icon.svg",
  xlsx: "/excel_icon.svg",
  ppt: "/PPT_icon.svg",
  pptx: "/PPT_icon.svg",
  md: "/md_icon.svg",
  markdown: "/md_icon.svg",
  txt: "/TXT_icon.svg",
  jpg: "/jpg_icon.svg",
  jpeg: "/jpg_icon.svg",
  png: "/png_icon.svg",
  gif: "/gif_icon.svg",
  webp: "/jpg_icon.svg",
  svg: "/jpg_icon.svg",
  bmp: "/jpg_icon.svg",
  ico: "/jpg_icon.svg",
  tiff: "/jpg_icon.svg",
  tif: "/jpg_icon.svg",
  mp3: "/mp3.svg",
  wav: "/mp3.svg",
  ogg: "/mp3.svg",
  m4a: "/mp3.svg",
  aac: "/mp3.svg",
  flac: "/mp3.svg",
  wma: "/mp3.svg",
  mp4: "/mp4.svg",
  avi: "/mp4.svg",
  mkv: "/mp4.svg",
  mov: "/mp4.svg",
  wmv: "/mp4.svg",
  flv: "/mp4.svg",
  webm: "/mp4.svg",
  m4v: "/mp4.svg",
  json: "/TXT_icon.svg",
};

// 获取文件图标(通过文件名,返回 SVG 路径)
export const getFileIconByFileName = (fileName: string): string => {
  if (!fileName) {
    return "/TXT_icon.svg";
  }
  const ext = getFileExtension(fileName);
  return ext && FILE_EXTENSION_TO_ICON[ext] ? FILE_EXTENSION_TO_ICON[ext] : "/wenjian.svg";
};

// 获取文件图标(通过 MIME 类型,返回 FontAwesome 类名,用于兼容旧代码)
export const getFileIcon = (fileType: string): string => {
  const fileInfo = SUPPORTED_FILE_TYPES[fileType];
  return fileInfo ? fileInfo.icon : "fas fa-file";
};

// 格式化文件大小
export const formatFileSize = (bytes: number): string => {
  if (bytes === 0) return "0 B";

  const k = 1024;
  const sizes = ["B", "KB", "MB", "GB"];
  const i = Math.floor(Math.log(bytes) / Math.log(k));

  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};

interface FileDragHandlerOptions {
  onDragEnter?: (event: DragEvent) => void;
  onDragLeave?: (event: DragEvent) => void;
  onDrop?: (data: {
    validFiles: File[];
    invalidFiles: { file: File; errors: string[] }[];
    event: DragEvent;
  }) => void;
}

// 文件拖拽处理类
export class FileDragHandler {
  private options: FileDragHandlerOptions;
  private isDragOver: boolean;
  private dragCounter: number;

  constructor(options: FileDragHandlerOptions = {}) {
    this.options = {
      onDragEnter: undefined,
      onDragLeave: undefined,
      onDrop: undefined,
      ...options,
    };

    this.isDragOver = false;
    this.dragCounter = 0;
  }

  // 绑定拖拽事件
  bindEvents(element: HTMLElement | null) {
    if (!element) return;

    element.addEventListener("dragenter", this.handleDragEnter.bind(this));
    element.addEventListener("dragleave", this.handleDragLeave.bind(this));
    element.addEventListener("dragover", this.handleDragOver.bind(this));
    element.addEventListener("drop", this.handleDrop.bind(this));
  }

  // 处理拖拽进入
  handleDragEnter(event: DragEvent) {
    event.preventDefault();
    event.stopPropagation();

    this.dragCounter++;

    if (!this.isDragOver) {
      this.isDragOver = true;
      if (this.options.onDragEnter) {
        this.options.onDragEnter(event);
      }
    }
  }

  // 处理拖拽离开
  handleDragLeave(event: DragEvent) {
    event.preventDefault();
    event.stopPropagation();

    this.dragCounter--;

    if (this.dragCounter === 0) {
      this.isDragOver = false;
      if (this.options.onDragLeave) {
        this.options.onDragLeave(event);
      }
    }
  }

  // 处理拖拽悬停
  handleDragOver(event: DragEvent) {
    event.preventDefault();
    event.stopPropagation();
  }

  // 处理文件拖放
  handleDrop(event: DragEvent) {
    event.preventDefault();
    event.stopPropagation();

    this.isDragOver = false;
    this.dragCounter = 0;

    const fileList = event.dataTransfer?.files;
    const files: File[] = fileList ? Array.prototype.slice.call(fileList) : [];
    const validFiles: File[] = [];
    const invalidFiles: { file: File; errors: string[] }[] = [];

    files.forEach((file: File) => {
      const validation = validateFile(file);
      if (validation.isValid) {
        validFiles.push(file);
      } else {
        invalidFiles.push({ file, errors: validation.errors });
      }
    });

    if (this.options.onDrop) {
      this.options.onDrop({ validFiles, invalidFiles, event });
    }
  }
}

export default {
  validateFile,
  getFileIcon,
  getFileIconByFileName,
  formatFileSize,
  FileDragHandler,
};