520 lines
15 KiB
JavaScript
520 lines
15 KiB
JavaScript
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("/");
|
|
}
|