文件下载和上传
通用 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,
};
}