import { getAliyunKeyApi, insertFileApi } from "@/request/api"; import { saveAs } from "file-saver"; import JSZip from "jszip"; import { ElMessage } from "element-plus"; /** * 选择并压缩图片 */ export function uploadImage(options = {}) { const { quality = 0.9, maxSize = 512000 } = options; return new Promise((resolve, reject) => { if (typeof document === "undefined") { reject(new Error("Require browser environment")); return; } const input = document.createElement("input"); input.type = "file"; input.accept = "image/*"; input.style.display = "none"; input.onchange = async (e) => { const file = e.target.files?.[0]; if (!file) { reject(new Error("No file selected")); return; } try { if (!file.type.startsWith("image/")) { throw new Error("Invalid image file"); } // 创建一个临时URL来加载图片 const url = URL.createObjectURL(file); // 调用 compressImage 进行压缩 const compressResult = await compressImage(url, quality, maxSize); // 释放临时URL URL.revokeObjectURL(url); resolve({ blob: compressResult.blob, url: compressResult.url, fileName: file.name, fileExt: getFileExtension(file.name), fileSize: file.size, }); } catch (err) { reject(err); } finally { document.body.removeChild(input); } }; document.body.appendChild(input); input.click(); }); } /** * 选择视频 */ export function uploadVideo() { return new Promise((resolve, reject) => { if (typeof document === "undefined") { reject(new Error("Require browser environment")); return; } const input = document.createElement("input"); input.type = "file"; input.accept = "video/*"; input.style.display = "none"; input.onchange = (e) => { const file = e.target.files?.[0]; if (!file) { reject(new Error("No file selected")); return; } if (!file.type.startsWith("video/")) { reject(new Error("Invalid video file")); return; } resolve({ blob: file, url: URL.createObjectURL(file), fileName: file.name, fileExt: getFileExtension(file.name), fileSize: file.size, }); document.body.removeChild(input); }; document.body.appendChild(input); input.click(); }); } // 压缩图片 export async function compressImage(url, quality = 0.9, maxSize = 512000) { return new Promise((resolve, reject) => { const img = new Image(); img.src = url; img.onload = async () => { try { // 获取原图大小 const imgSize = await fetch(url) .then((res) => res.blob()) .then((blob) => blob.size); if (imgSize <= maxSize) { return resolve({ url, size: imgSize, blob: await fetch(url).then((res) => res.blob()) }); } const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); if (!ctx) return reject(new Error("无法获取canvas上下文")); let targetWidth = img.width; let targetHeight = img.height; // 最大边长限制 const maxEdge = 2000; if (targetWidth > maxEdge || targetHeight > maxEdge) { if (targetWidth > targetHeight) { targetHeight = (maxEdge * targetHeight) / targetWidth; targetWidth = maxEdge; } else { targetWidth = (maxEdge * targetWidth) / targetHeight; targetHeight = maxEdge; } } canvas.width = targetWidth; canvas.height = targetHeight; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0, targetWidth, targetHeight); const getBlob = (q) => new Promise((resolve, reject) => { canvas.toBlob( (blob) => (blob ? resolve(blob) : reject(new Error("Blob转换失败"))), "image/jpeg", q, ); }); let blob = await getBlob(quality); // 调整质量直到接近最大文件大小 let minQ = 0.5; let maxQ = quality; let attempts = 0; while (blob.size > maxSize && maxQ - minQ > 0.01 && attempts < 5) { const midQ = (minQ + maxQ) / 2; blob = await getBlob(midQ); if (blob.size > maxSize) { maxQ = midQ; } else { minQ = midQ; } attempts++; } resolve({ blob, size: blob.size, url: URL.createObjectURL(blob) }); } catch (err) { reject(err); } }; img.onerror = () => reject(new Error("图片加载失败")); }); } export function formatFileSize(size) { if (size < 1024) return `${size} B`; else if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`; else if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(2)} MB`; else return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`; } // 获取文件扩展名并进行分类 export function getFileExtension(fileName) { // 获取文件扩展名 const fileExt = fileName.includes(".") ? fileName.slice(fileName.lastIndexOf(".") + 1).toLowerCase() // 获取扩展名并转为小写 : ""; // 如果是office类文件,将其归类到 "office" if (["docx", "docm", "xls", "xlsx"].includes(fileExt)) { return "office"; } // 返回扩展名 return fileExt; } export function downloadOssZip(files, fileName = "文件合集", callback) { const zip = new JSZip(); const addedFiles = new Set(); const tasks = files.map((item) => { return getAliyunFileBlob(item.mx_mapfilename) .then((res) => { const uniqueFileName = getUniqueFileName(item.mx_filename, addedFiles); zip.file(uniqueFileName, res, { binary: true }); addedFiles.add(uniqueFileName); }) .catch((err) => { // 捕获单个文件下载失败的错误,不影响其他文件下载 console.error(`Failed to download file: ${item.mx_filename}`, err); }); }); // 关键:使用 Promise.all 确保所有任务完成 return Promise.all(tasks) .then(() => zip.generateAsync({ type: "blob" })) .then((res) => { saveAs(res, `${fileName}.zip`); callback && callback(); }) .catch((err) => { callback && callback(); throw err; }); } // 处理文件名重复的函数 function getUniqueFileName(fileName, addedFiles) { let uniqueFileName = fileName; let counter = 1; // 如果文件名已经存在,则在文件名后加上 (1), (2), ... while (addedFiles.has(uniqueFileName)) { const fileNameWithoutExtension = uniqueFileName.replace(/\.[^/.]+$/, ""); const extension = uniqueFileName.slice(uniqueFileName.lastIndexOf(".")); uniqueFileName = `${fileNameWithoutExtension} (${counter})${extension}`; counter++; } return uniqueFileName; } // ============================================ 阿里云上传和下载 ============================================ let cachedCredentials = null; // 用来缓存凭证 let isFetchingCredentials = false; // 控制是否正在请求凭证 // 获取阿里云文件的上传凭证 export function getAliyunUploadCredentials() { return new Promise((resolve, reject) => { // 如果缓存中有有效的凭证,则直接使用 if (cachedCredentials && !isCredentialsExpired(cachedCredentials)) { resolve(cachedCredentials); // 凭证未过期,直接返回缓存的凭证 } else { // 如果正在请求凭证,则等待 if (isFetchingCredentials) { const interval = setInterval(() => { // 等待直到凭证被请求完毕 if (!isFetchingCredentials) { clearInterval(interval); // 请求完成后停止轮询 resolve(cachedCredentials); // 返回缓存的凭证 } }, 100); // 每100ms检查一次 } else { // 如果凭证过期,重新请求新的凭证 isFetchingCredentials = true; // 设置为正在请求凭证状态 getAliyunKeyApi() .then((res) => { if (res.success) { const { accessid, policy, signature, expire, dir, host } = res.data; // 缓存凭证,并记录过期时间 cachedCredentials = { accessid, policy, signature, expire: parseInt(expire, 10), dir, host, }; isFetchingCredentials = false; // 请求完成,解除锁定 resolve(cachedCredentials); // 返回新凭证 } else { isFetchingCredentials = false; // 请求失败,解除锁定 reject(new Error("获取凭证失败")); } }) .catch((err) => { isFetchingCredentials = false; // 请求失败,解除锁定 reject(new Error("请求凭证失败:" + err.message)); }); } } }); } // 上传文件到阿里云 export function uploadFile(blob, fileName, fileExt, fileSize, xh, moduleid, father, cateid) { return new Promise((resolve, reject) => { getAliyunUploadCredentials() .then(async (aliyunKey) => { try { const formData = new FormData(); let uniqueName = `${Date.now()}_${Math.random().toString(36).slice(2)}`; if (fileExt) { uniqueName += `.${fileExt}`; } // 构造上传的文件路径 formData.append("key", `${aliyunKey.dir}${uniqueName}`); formData.append("OSSAccessKeyId", aliyunKey.accessid); formData.append("policy", aliyunKey.policy); formData.append("Signature", aliyunKey.signature); formData.append("file", blob, uniqueName); console.log(formData); console.log(aliyunKey.host); console.log(aliyunKey.dir); const xhr = new XMLHttpRequest(); xhr.open("POST", aliyunKey.host, true); xhr.onload = async () => { if (xhr.status === 200 || xhr.status === 204) { const allFilePath = `${aliyunKey.host}/${aliyunKey.dir}${uniqueName}`; // 上传后处理(插入文件记录等) const insertRes = await insertFileApi({ father, mx_moduleid: moduleid, mx_xh: xh, mx_filename: fileName, mx_mapfilename: allFilePath, mx_filesize: fileSize, mx_fileext: fileExt, mx_cate_id: cateid || "", }); if (insertRes.success) { resolve({ father, mx_moduleid: moduleid, mx_xh: xh, mx_filename: fileName, mx_mapfilename: allFilePath, mx_filesize: fileSize, mx_fileext: fileExt, mx_cate_id: cateid || "", }); } else { reject(new Error(insertRes.data.message)); } } else { ElMessage.error("上传失败,请稍后再试"); reject(new Error(`OSS 上传失败: ${xhr.responseText}`)); } }; xhr.onerror = () => { ElMessage.error("上传失败,请稍后再试"); reject(new Error("OSS 上传异常")); }; xhr.send(formData); } catch (err) { ElMessage.error("上传失败,请稍后再试"); reject(err); } }) .catch(reject); }); } // 检查凭证是否过期 function isCredentialsExpired(credentials) { const currentTime = Math.floor(Date.now() / 1000); return currentTime > credentials.expire; } // 获取阿里云文件的 Blob 数据(用于下载) export function getAliyunFileBlob(url) { return new Promise((resolve, reject) => { getAliyunUploadCredentials() .then((aliyunKey) => { const downloadUrl = `${url}?OSSAccessKeyId=${aliyunKey.accessid}&policy=${aliyunKey.policy}&Signature=${aliyunKey.signature}`; fetch(downloadUrl, { mode: "cors" }) .then((response) => { if (!response.ok) { throw new Error(`下载失败,状态码:${response.status}`); } return response.blob(); }) .then((blob) => { resolve(blob); }) .catch((error) => { reject(error); }); }) .catch(reject); }); } // ============================================ 阿里云上传和下载 ============================================ // ============================================ 本地文件上传和下载 ========================================== export function buildPublicUrl(filePath) { const baseUrl = import.meta.env.PUBLIC_API_BASE_URL || ""; return joinUrl(baseUrl, "/static/file", filePath); } /** * 从完整 URL 中提取 filePath * @param fullUrl 完整访问地址 * @returns filePath */ export function extractFilePath(fullUrl) { if (!fullUrl) return ""; const baseUrl = import.meta.env.PUBLIC_API_BASE_URL || ""; // 去掉 baseUrl let path = fullUrl.startsWith(baseUrl) ? fullUrl.slice(baseUrl.length) : fullUrl; // 去掉 /static/file 前缀 const prefix = "/static/file"; if (path.startsWith(prefix)) { path = path.slice(prefix.length); } // 去掉开头的 / if (path.startsWith("/")) { path = path.slice(1); } return path; } export function getFileBlob(path) { const fullUrl = buildPublicUrl(path); return fetch(fullUrl).then((response) => { if (!response.ok) { throw new Error(`获取文件失败,状态码:${response.status}`); } return response.blob(); }); } export function downloadZip(files, fileName = "文件合集", callback) { const zip = new JSZip(); const addedFiles = new Set(); const tasks = files.map((item) => { return getFileBlob(item.mx_mapfilename) .then((res) => { const uniqueFileName = getUniqueFileName(item.mx_filename, addedFiles); zip.file(uniqueFileName, res, { binary: true }); addedFiles.add(uniqueFileName); }) .catch((err) => { // 捕获单个文件下载失败的错误,不影响其他文件下载 console.error(`Failed to download file: ${item.mx_filename}`, err); }); }); // 关键:使用 Promise.all 确保所有任务完成 return Promise.all(tasks) .then(() => zip.generateAsync({ type: "blob" })) .then((res) => { saveAs(res, `${fileName}.zip`); callback && callback(); }) .catch((err) => { callback && callback(); throw err; }); } // ============================================ 本地文件上传和下载 ========================================== export function joinUrl(...parts) { if (!parts || parts.length === 0) return ""; const sb = []; for (let part of parts) { if (!part) continue; part = String(part).trim(); if (!part) continue; // 替换 Windows 路径分隔符 part = part.replace(/\\/g, "/"); if (sb.length === 0) { // 第一段:保留协议(http:// https://),去掉末尾 / sb.push(part.replace(/\/+$/, "")); } else { // 后续段:去掉前后 / sb.push(part.replace(/^\/+|\/+$/g, "")); } } return sb.join("/"); }