Home
avatar

.Demure

文件下载和上传

通用 button 触发创建Blob文件流下载,和 el-upload 的上传

useFileUpload.ts

import { ref } from "vue";
import { ElMessage } from "element-plus";
import type { UploadFile, UploadFiles, UploadUserFile } from "element-plus";

const isAllowedFileType = (
  file: UploadFile,
  allowedTypes: readonly string[],
  allowedExtensions: readonly string[]
): boolean => {
  if (!file.raw) return false;
  const extension = file.name.split(".").pop()?.toLowerCase();
  if (allowedTypes.includes(file.raw.type)) {
    return true;
  }

  // 如果 MIME 类型不匹配,但扩展名匹配,则根据扩展名判断
  if (extension && allowedExtensions.includes(extension)) {
    if (file.raw.type === "application/octet-stream") {
      return true; // RAR 文件通常显示为 octet-stream
    }
    return true;
  }
  return false;
};

/**
 * 文件验证规则接口
 */
export interface FileValidationRules {
  allowedTypes: readonly string[];
  maxSize: number; // MB
  allowedExtensions: readonly string[];
  namePattern?: RegExp;
}

/**
 * 文件上传 Composable
 * 统一处理文件上传、验证、管理逻辑
 */
export function useFileUpload(rules: FileValidationRules) {
  const fileList = ref<UploadUserFile[]>([]);
  const uploadLoading = ref(false);

  /**
   * 验证文件
   */
  const validateFile = (file: UploadFile): boolean => {
    // 文件大小验证
    if (file.raw && file.raw.size > rules.maxSize * 1024 * 1024) {
      ElMessage.error(`文件大小不能超过${rules.maxSize}MB`);
      return false;
    }

    // 文件类型验证(改进 RAR 文件检测)
    if (
      file.raw &&
      !isAllowedFileType(file, rules.allowedTypes, rules.allowedExtensions)
    ) {
      ElMessage.error("上传失败,文件格式错误,请修改后重新上传");
      return false;
    }

    // 文件扩展名验证
    const extension = file.name.split(".").pop()?.toLowerCase();
    if (extension && !rules.allowedExtensions.includes(extension)) {
      ElMessage.error(`仅支持${rules.allowedExtensions.join("、")}格式的文件`);
      return false;
    }

    // 文件名验证(不包含扩展名)
    if (rules.namePattern) {
      const nameWithoutExtension = file.name.split(".").slice(0, -1).join(".");
      if (!rules.namePattern.test(nameWithoutExtension)) {
        ElMessage.error(
          "文件名仅支持中文、英文、数字、下划线、横杠,请修改后重新上传"
        );
        return false;
      }
    }
    return true;
  };

  /**
   * 处理文件变化
   */
  const handleFileChange = (
    uploadFile: UploadFile,
    uploadFiles: UploadFiles
  ) => {
    if (!validateFile(uploadFile)) {
      // 移除无效文件
      fileList.value = fileList.value.filter(
        (item) => item.uid !== uploadFile.uid
      );
      return;
    }
    fileList.value = uploadFiles;
  };

  /**
   * 处理文件移除
   */
  const handleFileRemove = (file: UploadFile) => {
    fileList.value = fileList.value.filter((item) => item.uid !== file.uid);
  };

  /**
   * 重置文件列表
   */
  const resetFiles = () => {
    fileList.value = [];
  };

  /**
   * 验证是否有文件
   */
  const validateHasFiles = (): boolean => {
    if (fileList.value.length === 0) {
      ElMessage.warning("请先选择要上传的文件");
      return false;
    }
    return true;
  };

  /**
   * 获取有效的文件列表
   */
  const getValidFiles = (): File[] => {
    const validFiles: File[] = [];
    for (const item of fileList.value) {
      if (item.raw && item.raw instanceof File) {
        validFiles.push(item.raw);
      }
    }
    return validFiles;
  };

  return {
    fileList,
    validateFile,
    uploadLoading,
    handleFileChange,
    handleFileRemove,
    resetFiles,
    validateHasFiles,
    getValidFiles,
    setUploadLoading: (loading: boolean) => {
      uploadLoading.value = loading;
    },
  };
}

/**
 * 预定义的文件验证规则
 */
export const FileValidationPresets = {
  // Excel文件
  excel: {
    allowedTypes: [
      "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
      "application/vnd.ms-excel",
    ],
    maxSize: 10,
    allowedExtensions: ["xlsx", "xls"],
    namePattern: /^[\u4e00-\u9fa5a-zA-Z0-9_-]+$/,
  },

  // 图片文件
  image: {
    allowedTypes: ["image/jpeg", "image/jpg", "image/png"],
    maxSize: 5,
    allowedExtensions: ["jpg", "jpeg", "png"],
    namePattern: /^[\u4e00-\u9fa5_a-zA-Z0-9\-]+$/,
  },

  // 自证材料文件
  proofMaterial: {
    allowedTypes: [
      "image/jpeg",
      "image/jpg",
      "image/png",
      "image/gif",
      "application/pdf",
      "application/vnd.openxmlformats-officedocument.presentationml.presentation",
      "application/x-rar-compressed",
      "application/vnd.rar",
      "application/x-rar",
      "application/octet-stream", // 通用二进制类型,包含 RAR
      "application/zip",
      "application/x-zip-compressed",
    ],
    maxSize: 10,
    allowedExtensions: [
      "jpg",
      "jpeg",
      "png",
      "gif",
      "pdf",
      "pptx",
      "rar",
      "zip",
    ],
    namePattern: /^[\u4e00-\u9fa5_a-zA-Z0-9\-]+$/,
  },
} as const;

useFileDownload.ts

import { ref } from "vue";
import { ElMessage } from "element-plus";

/**
 * 文件下载 Composable
 * 统一处理文件下载和模板导出逻辑
 */
export function useFileDownload() {
  const downloading = ref(false);

  /**
   * 通用文件下载方法
   */
  const downloadFile = (url: string, filename: string) => {
    if (!url) {
      ElMessage.warning("文件链接不存在");
      return;
    }

    try {
      const link = document.createElement("a");
      link.href = url;
      link.download = filename;
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
    } catch (error) {
      console.error("Download error:", error);
      ElMessage.error("文件下载失败,请稍后重试");
    }
  };

  /**
   * Blob数据下载方法
   */
  const downloadBlob = (blob: Blob, filename: string) => {
    if (blob.size === 0) {
      ElMessage.info("内容为空,无法下载");
      return;
    }

    try {
      const url = window.URL.createObjectURL(blob);
      const link = document.createElement("a");
      link.href = url;
      link.download = filename;
      document.body.appendChild(link);
      link.click();

      // 清理资源
      window.setTimeout(() => {
        window.URL.revokeObjectURL(url);
        document.body.removeChild(link);
      }, 0);
    } catch (error) {
      console.error("Download error:", error);
      ElMessage.error("文件下载失败,请稍后重试");
    }
  };

  /**
   * 带加载状态的模板下载
   */
  const downloadTemplate = async (
    downloadFn: () => Promise<any>,
    filename: string,
    successMessage?: string
  ) => {
    downloading.value = true;

    try {
      const response = await downloadFn();

      if (!response) {
        ElMessage.info("导出失败,未获取到数据");
        return;
      }

      // 创建Blob并下载
      const blob = new Blob([response.data], {
        type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
      });

      downloadBlob(blob, filename);

      if (successMessage) {
        ElMessage.success(successMessage);
      }
    } catch (error: any) {
      ElMessage.error(error.msg || "模板下载失败");
    } finally {
      downloading.value = false;
    }
  };

  return {
    downloading,
    downloadFile,
    downloadBlob,
    downloadTemplate,
  };
}
Code

喜欢这篇文章嘛,觉得文章不错的话,奖励奖励我!

支付宝打赏支付宝微信打赏 微信