|
@@ -0,0 +1,448 @@
|
|
|
+<template>
|
|
|
+ <div>
|
|
|
+ <el-upload
|
|
|
+ ref="upload"
|
|
|
+ action=""
|
|
|
+ :on-remove="handleRemove"
|
|
|
+ :on-error="error"
|
|
|
+ :http-request="toUpload"
|
|
|
+ :auto-upload="true"
|
|
|
+ :file-list="fileList"
|
|
|
+ :limit="limit"
|
|
|
+ :on-exceed="handleExceed"
|
|
|
+ multiple
|
|
|
+ >
|
|
|
+ <el-button size="small" type="primary">点击上传</el-button>
|
|
|
+ </el-upload>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import SparkMD5 from "spark-md5";
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: "uploadBigFile",
|
|
|
+ props: {
|
|
|
+ // 限制上传的个数
|
|
|
+ limit: {
|
|
|
+ type: Number,
|
|
|
+ default: 100,
|
|
|
+ },
|
|
|
+ // 一组文件传相同的值,保证其为一组
|
|
|
+ objectPath: {
|
|
|
+ type: String,
|
|
|
+ default: "1",
|
|
|
+ },
|
|
|
+ type: {
|
|
|
+ type: String,
|
|
|
+ default: "1",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ fileList: [],
|
|
|
+ attachmentList: [],
|
|
|
+ defaultParam: {},
|
|
|
+ chunkSize: 5 * 1024 * 1024, // 每次切片的大小
|
|
|
+ uploadIdInfo: {},
|
|
|
+ simultaneousUploads: 3, // 上传并发数
|
|
|
+ };
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ handleExceed(files, fileList) {
|
|
|
+ this.$message.warning(
|
|
|
+ `当前限制选择 ${this.limit} 个文件,本次选择了 ${
|
|
|
+ files.length
|
|
|
+ } 个文件,共选择了 ${files.length + fileList.length} 个文件`
|
|
|
+ );
|
|
|
+ },
|
|
|
+ async handleRemove(file, fileList) {
|
|
|
+ let md5 = "";
|
|
|
+ if (file.raw) {
|
|
|
+ await this.$md5(file.raw).then((res) => {
|
|
|
+ md5 = res;
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ md5 = file.md5;
|
|
|
+ }
|
|
|
+
|
|
|
+ let removeIndex = "";
|
|
|
+ this.attachmentList.forEach((item, index) => {
|
|
|
+ if (md5 === item.md5) {
|
|
|
+ removeIndex = index;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ this.attachmentList.splice(removeIndex, 1);
|
|
|
+ this.$emit("attachmentChange", this.attachmentList);
|
|
|
+ },
|
|
|
+
|
|
|
+ async toUpload(file) {
|
|
|
+ let currentFile = file.file;
|
|
|
+
|
|
|
+ // 上传前进行通知
|
|
|
+ this.$emit("willUpload");
|
|
|
+
|
|
|
+ // 1. 计算MD5
|
|
|
+ await this.getFileMd5(currentFile, async (md5, totalChunks) => {
|
|
|
+ // 2. 检查是否已上传
|
|
|
+ // const checkResult = await this.checkFileUploadedByMd5(md5);
|
|
|
+ // 已上传
|
|
|
+ // if (checkResult.data.status === 1) {
|
|
|
+ // this.$message.success(
|
|
|
+ // `上传成功,文件地址:${checkResult.data.url}`
|
|
|
+ // );
|
|
|
+ // console.log("文件访问地址:" + checkResult.data.url);
|
|
|
+ // // currentFile.status = FileStatus.success;
|
|
|
+ // // currentFile.uploadProgress = 100;
|
|
|
+ // return;
|
|
|
+ // } else if (checkResult.data.status === 2) {
|
|
|
+ // // "上传中" 状态
|
|
|
+ // // 获取已上传分片列表
|
|
|
+ // let chunkUploadedList = checkResult.data.chunkUploadedList;
|
|
|
+ // currentFile.chunkUploadedList = chunkUploadedList;
|
|
|
+ // } else {
|
|
|
+ // // 未上传
|
|
|
+ // console.log("未上传");
|
|
|
+ // }
|
|
|
+
|
|
|
+ // console.log("文件MD5:" + md5);
|
|
|
+ // 3. 正在创建分片
|
|
|
+ let chunkSize = this.chunkSize;
|
|
|
+
|
|
|
+ let fileChunks = this.createFileChunk(currentFile, chunkSize);
|
|
|
+
|
|
|
+ let param = {
|
|
|
+ // fileName: currentFile.name,
|
|
|
+ // fileSize: currentFile.size,
|
|
|
+ // chunkSize: chunkSize,
|
|
|
+ // fileMd5: md5,
|
|
|
+ // contentType: "application/octet-stream",
|
|
|
+ objectName: `${this.objectPath}/${currentFile.name}`,
|
|
|
+ chunkSize: totalChunks,
|
|
|
+ type: "algorithmFile",
|
|
|
+ };
|
|
|
+ // 4. 获取上传url
|
|
|
+ let uploadIdInfoResult = await this.getFileUploadUrls(param);
|
|
|
+ if (uploadIdInfoResult) {
|
|
|
+ this.uploadIdInfo = uploadIdInfoResult;
|
|
|
+ } else {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let uploadUrls = this.uploadIdInfo.uploadUrls;
|
|
|
+ if (fileChunks.length !== uploadUrls.length) {
|
|
|
+ this.$message.error("文件分片上传地址获取错误!");
|
|
|
+ this.$emit("didUpload");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.$set(currentFile, "chunkList", []);
|
|
|
+
|
|
|
+ fileChunks.map((chunkItem, index) => {
|
|
|
+ currentFile.chunkList.push({
|
|
|
+ chunkNumber: index + 1,
|
|
|
+ chunk: chunkItem,
|
|
|
+ uploadUrl: uploadUrls[index],
|
|
|
+ uploadTimesLeft: 3, // 剩余可上传次数
|
|
|
+ });
|
|
|
+ });
|
|
|
+ let tempFileChunks = [];
|
|
|
+ currentFile.chunkList.forEach((item) => {
|
|
|
+ tempFileChunks.push(item);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 处理分片列表,删除已上传的分片
|
|
|
+ // tempFileChunks = this.processUploadChunkList(tempFileChunks,currentFile);
|
|
|
+
|
|
|
+ // 5. 上传
|
|
|
+ let isUploaded = await this.uploadChunkBase(tempFileChunks);
|
|
|
+ // console.log("上传完成", isUploaded);
|
|
|
+ if (!isUploaded) {
|
|
|
+ this.$message.error("上传失败");
|
|
|
+ this.$emit("didUpload");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 合并文件
|
|
|
+ const mergeResult = await this.mergeFile({
|
|
|
+ uploadId: this.uploadIdInfo.uploadId,
|
|
|
+ fileName: currentFile.name,
|
|
|
+ md5: md5,
|
|
|
+ });
|
|
|
+
|
|
|
+ if (mergeResult) {
|
|
|
+ this.success();
|
|
|
+ this.attachmentList.push({
|
|
|
+ fileName: currentFile.name,
|
|
|
+ });
|
|
|
+ this.$emit("attachmentChange", this.attachmentList);
|
|
|
+ }
|
|
|
+ this.$emit("didUpload");
|
|
|
+ // if (!mergeResult.success) {
|
|
|
+ // // currentFile.status = FileStatus.error;
|
|
|
+ // this.$message.error(mergeResult.error);
|
|
|
+ // } else {
|
|
|
+ // // currentFile.status = FileStatus.success;
|
|
|
+ // console.log("文件访问地址:" + mergeResult.data.url);
|
|
|
+ // this.$message.success(
|
|
|
+ // `上传成功,文件地址:${mergeResult.data.url}`
|
|
|
+ // );
|
|
|
+ // }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /* checkFileUploadedByMd5(md5) {
|
|
|
+ let url = `http://127.0.0.1:8027/upload/check?md5=${md5}`
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ axios.get(url).then((response) => {
|
|
|
+ resolve(response.data)
|
|
|
+ }).catch(error => {
|
|
|
+ reject(error)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }, */
|
|
|
+ getFileUploadUrls(fileParam) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ this.$axios({
|
|
|
+ method: "post",
|
|
|
+ url: this.$api.common.createMultipartUpload,
|
|
|
+ data: fileParam,
|
|
|
+ })
|
|
|
+ .then((res) => {
|
|
|
+ if (res.code == 200 && res.info) {
|
|
|
+ resolve(res.info);
|
|
|
+ } else {
|
|
|
+ this.$emit("didUpload");
|
|
|
+ this.$message.error(
|
|
|
+ res.message || "文件分片上传地址获取错误"
|
|
|
+ );
|
|
|
+ resolve(false);
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ this.$emit("didUpload");
|
|
|
+ this.$message.error(
|
|
|
+ error || "文件分片上传地址获取错误!"
|
|
|
+ );
|
|
|
+ resolve(false);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 分片读取文件 MD5
|
|
|
+ */
|
|
|
+ getFileMd5(file, callback) {
|
|
|
+ let chunkSize = this.chunkSize;
|
|
|
+ const blobSlice =
|
|
|
+ File.prototype.slice ||
|
|
|
+ File.prototype.mozSlice ||
|
|
|
+ File.prototype.webkitSlice;
|
|
|
+ const fileReader = new FileReader();
|
|
|
+ // 计算分片数
|
|
|
+ const totalChunks = Math.ceil(file.size / chunkSize);
|
|
|
+ // console.log("总分片数:" + totalChunks);
|
|
|
+ let currentChunk = 0;
|
|
|
+ const spark = new SparkMD5.ArrayBuffer();
|
|
|
+
|
|
|
+ loadNext();
|
|
|
+
|
|
|
+ fileReader.onload = function (e) {
|
|
|
+ try {
|
|
|
+ spark.append(e.target.result);
|
|
|
+ } catch (error) {
|
|
|
+ console.log("获取Md5错误:" + currentChunk);
|
|
|
+ // 上传完成后进行通知
|
|
|
+ this.$emit("didUpload");
|
|
|
+ this.$message.error("文件解析错误");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (currentChunk < totalChunks) {
|
|
|
+ currentChunk++;
|
|
|
+ loadNext();
|
|
|
+ } else {
|
|
|
+ callback(spark.end(), totalChunks);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ fileReader.onerror = function () {
|
|
|
+ console.warn("读取Md5失败,文件读取错误");
|
|
|
+ // 上传完成后进行通知
|
|
|
+ this.$emit("didUpload");
|
|
|
+ this.$message.error("文件读取错误");
|
|
|
+ };
|
|
|
+
|
|
|
+ function loadNext() {
|
|
|
+ const start = currentChunk * chunkSize;
|
|
|
+ const end =
|
|
|
+ start + chunkSize >= file.size
|
|
|
+ ? file.size
|
|
|
+ : start + chunkSize;
|
|
|
+ // 注意这里的 fileRaw
|
|
|
+ fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 文件分片
|
|
|
+ */
|
|
|
+ createFileChunk(file, size = chunkSize) {
|
|
|
+ const fileChunkList = [];
|
|
|
+ let count = 0;
|
|
|
+ while (count < file.size) {
|
|
|
+ fileChunkList.push({
|
|
|
+ file: file.slice(count, count + size),
|
|
|
+ });
|
|
|
+ count += size;
|
|
|
+ }
|
|
|
+ return fileChunkList;
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 处理即将上传的分片列表,判断是否有已上传的分片,有则从列表中删除
|
|
|
+ */
|
|
|
+ /* processUploadChunkList(chunkList,file) {
|
|
|
+ // const currentFile = this.uploadFileList[currentFileIndex]
|
|
|
+ const currentFile = file
|
|
|
+ let chunkUploadedList = currentFile.chunkUploadedList
|
|
|
+ if (chunkUploadedList === undefined || chunkUploadedList === null || chunkUploadedList.length === 0) {
|
|
|
+ return chunkList
|
|
|
+ }
|
|
|
+ //
|
|
|
+ for (let i = chunkList.length - 1; i >= 0; i--) {
|
|
|
+ const chunkItem = chunkList[i]
|
|
|
+ for (let j = 0; j < chunkUploadedList.length; j++) {
|
|
|
+ if (chunkItem.chunkNumber === chunkUploadedList[j]) {
|
|
|
+ chunkList.splice(i, 1)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return chunkList
|
|
|
+ }, */
|
|
|
+
|
|
|
+ uploadChunkBase(chunkList) {
|
|
|
+ let successCount = 0;
|
|
|
+ let totalChunks = chunkList.length;
|
|
|
+
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const handler = () => {
|
|
|
+ if (chunkList.length) {
|
|
|
+ const chunkItem = chunkList.shift();
|
|
|
+ // console.log(chunkItem);
|
|
|
+
|
|
|
+ // 直接上传二进制,不需要构造 FormData,否则上传后文件损坏
|
|
|
+ this.$instance
|
|
|
+ .put(chunkItem.uploadUrl, chunkItem.chunk.file, {
|
|
|
+ // 上传进度处理
|
|
|
+ // onUploadProgress: this.checkChunkUploadProgress(chunkItem),
|
|
|
+ headers: {
|
|
|
+ "Content-Type": "application/octet-stream",
|
|
|
+ // 'Content-Type': 'binary/octet-stream'
|
|
|
+ // 'Content-Type': 'multipart/octet-stream'
|
|
|
+ },
|
|
|
+ })
|
|
|
+ .then((response) => {
|
|
|
+ successCount++;
|
|
|
+ this.$emit(
|
|
|
+ "percentageChange",
|
|
|
+ Math.floor(
|
|
|
+ (successCount / totalChunks) * 100
|
|
|
+ )
|
|
|
+ );
|
|
|
+
|
|
|
+ handler();
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ // 更新状态
|
|
|
+ // 重新添加到队列中
|
|
|
+ chunkItem.uploadTimesLeft--;
|
|
|
+
|
|
|
+ if (chunkItem.uploadTimesLeft < 0) {
|
|
|
+ resolve(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ chunkList.push(chunkItem);
|
|
|
+ handler();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (successCount >= totalChunks) {
|
|
|
+ resolve(true);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ // 并发
|
|
|
+ for (let i = 0; i < this.simultaneousUploads; i++) {
|
|
|
+ handler();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 合并文件
|
|
|
+ */
|
|
|
+ mergeFile(file) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ this.$axios({
|
|
|
+ method: "post",
|
|
|
+ url: this.$api.common.completeMultipartUpload,
|
|
|
+ data: {
|
|
|
+ objectName: this.uploadIdInfo.objectName,
|
|
|
+ uploadId: this.uploadIdInfo.uploadId,
|
|
|
+ },
|
|
|
+ })
|
|
|
+ .then((res) => {
|
|
|
+ if (res.code == 200) {
|
|
|
+ resolve(true);
|
|
|
+ } else {
|
|
|
+ resolve(false);
|
|
|
+ this.$message.error(res.message || "合并文件失败");
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ this.$message.error(error || "合并文件失败!");
|
|
|
+ resolve(false);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ success(response, file, fileList) {
|
|
|
+ this.$message.success("上传成功 ");
|
|
|
+ },
|
|
|
+ error(response, file, fileList) {
|
|
|
+ console.log("error 上传失败");
|
|
|
+ this.$message.warning("上传失败");
|
|
|
+ },
|
|
|
+ download(downPath, downName) {
|
|
|
+ this.$axios({
|
|
|
+ method: "post",
|
|
|
+ url: this.$api.common.download,
|
|
|
+ responseType: "blob",
|
|
|
+ data: {
|
|
|
+ objectName: downPath,
|
|
|
+ },
|
|
|
+ }).then((res) => {
|
|
|
+ const blob = new Blob([res]); //构造一个blob对象来处理数据
|
|
|
+ const fileName = downName;
|
|
|
+ //对于<a>标签,只有 Firefox 和 Chrome(内核) 支持 download 属性
|
|
|
+ //IE10以上支持blob但是依然不支持download
|
|
|
+ if ("download" in document.createElement("a")) {
|
|
|
+ //支持a标签download的浏览器
|
|
|
+ const link = document.createElement("a"); //创建a标签
|
|
|
+ link.download = fileName; //a标签添加属性
|
|
|
+ link.style.display = "none";
|
|
|
+ link.href = URL.createObjectURL(blob);
|
|
|
+ document.body.appendChild(link);
|
|
|
+ link.click(); //执行下载
|
|
|
+ URL.revokeObjectURL(link.href); //释放url
|
|
|
+ document.body.removeChild(link); //释放标签
|
|
|
+ } else {
|
|
|
+ //其他浏览器
|
|
|
+ navigator.msSaveBlob(blob, fileName);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ },
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+</style>
|