翼度科技»论坛 编程开发 JavaScript 查看内容

前端大文件分片MinIO上传的详细代码

6

主题

6

帖子

18

积分

新手上路

Rank: 1

积分
18
大文件分片上传是一种将大文件拆分成多个小文件片段进行上传的技术。这种方式可以提高上传效率,减少网络传输时间,并且在网络不稳定或者上传过程中出现错误时,可以更容易地恢复上传进度。

大文件分片上传的步骤如下:


  • 将大文件分成多个固定大小的片段,通常每个片段的大小在几十KB到几MB之间。
  • 逐个上传每个文件片段,可以使用HTTP、FTP等协议进行传输。
  • 服务器接收到每个文件片段后,判断其MD5值进行保存或者合并操作。
  • 在上传过程中,通过MD5值维护一个上传进度记录,标记已经上传成功的文件片段,以便在上传中断后能够恢复上传进度。
  • 当所有文件片段都上传完成后,服务器将文件片段进行合并,得到完整的大文件。

大文件分片上传的好处有:


  • 提高上传速度:将大文件拆分成小片段,可以同时上传多个片段,从而提高上传速度。
  • 断点续传:如果在上传过程中发生中断或者错误,可以根据上传进度记录,只重新上传丢失或者出错的文件片段,从而减少网络传输时间。
  • 易于管理:将大文件拆分成小片段,可以更方便地管理和存储,避免了一次性上传整个大文件可能导致的内存占用问题。
大文件分片上传技术已经广泛应用于各种云存储、文件传输等领域,为用户提供了更好的上传体验和效率。

视图代码

大文件分片需要读取时间所以要给加载状态,下面例子只适合单文件上传且带上传进度展示
  1. <template>
  2.   <div class="slice-upload" v-loading="loading" element-loading-text="文件分片读取中"
  3.     element-loading-spinner="el-icon-loading">
  4.     <form id="fromCont" method="post" style="display: inline-block">
  5.       <el-button size="small" @click="inputChange" class="file-choose-btn" :disabled="uploading">
  6.         选择文件
  7.         <input v-show="false" id="file" ref="fileValue" :accept="accept" type="file" @change="choseFile" />
  8.       </el-button>
  9.     </form>
  10.     <slot name="status"></slot>
  11.     <div class="el-upload__tip">
  12.       请上传不超过 <span style="color: #e6a23c">{{ maxCalc }}</span> 的文件
  13.     </div>
  14.     <div class="file-list">
  15.       <transition name="list" tag="p">
  16.         <div v-if="file" class="list-item">
  17.           <i class="el-icon-document mr5"></i>
  18.           <span>{{ file.name }}
  19.             <em v-show="uploading" style="color: #67c23a">上传中....</em></span>
  20.           <span class="percentage">{{ percentage }}%</span>
  21.           <el-progress :show-text="false" :text-inside="false" :stroke-width="2" :percentage="percentage" />
  22.         </div>
  23.       </transition>
  24.     </div>
  25.   </div>
  26. </template>
复制代码
逻辑代码

需要引入Md5
  1. npm install spark-md5
复制代码
  1. <script>
  2. import SparkMD5 from "spark-md5";
  3. import axios from "axios";
  4. import {
  5.   getImagecheckFile,//检验是否上传过用于断点续传
  6.   Imageinit,//用分片换取minIo上传地址
  7.   Imagecomplete,//合并分片
  8. } from "/*接口地址*/";
  9. export default {
  10.   name: "sliceUpload",
  11.   /**
  12.    * 外部数据
  13.    * @type {Object}
  14.    */
  15.   props: {
  16.     /**
  17.      * @Description
  18.      * 代码注释说明
  19.      * 接口url
  20.      * @Return
  21.      */
  22.     findFileUrl: String,
  23.     continueUrl: String,
  24.     finishUrl: String,
  25.     removeUrl: String,
  26.     /**
  27.      * @Description
  28.      * 代码注释说明
  29.      * 最大上传文件大小 100G
  30.      * @Return
  31.      */
  32.     maxFileSize: {
  33.       type: Number,
  34.       default: 100 * 1024 * 1024 * 1024,
  35.     },
  36.     /**
  37.      * @Description
  38.      * 代码注释说明
  39.      * 切片大小
  40.      * @Return
  41.      */
  42.     sliceSize: {
  43.       type: Number,
  44.       default: 50 * 1024 * 1024,
  45.     },
  46.     /**
  47.      * @Description
  48.      * 代码注释说明
  49.      * 是否可以上传
  50.      * @Return
  51.      */
  52.     show: {
  53.       type: Boolean,
  54.       default: true,
  55.     },
  56.     accept: String,
  57.   },
  58.   /**
  59.    * 数据定义
  60.    * @type {Object}
  61.    */
  62.   data() {
  63.     return {
  64.       /**
  65.        * @Description
  66.        * 代码注释说明
  67.        * 文件
  68.        * @Return
  69.        */
  70.       file: null,//源文件
  71.       imageSize: 0,//文件大小单位GB
  72.       uploadId: "",//上传id
  73.       fullPath: "",//上传地址
  74.       uploadUrls: [],//分片上传地址集合
  75.       hash: "",//文件MD5
  76.       /**
  77.        * @Description
  78.        * 代码注释说明
  79.        * 分片文件
  80.        * @Return
  81.        */
  82.       formDataList: [],
  83.       /**
  84.        * @Description
  85.        * 代码注释说明
  86.        * 未上传分片
  87.        * @Return
  88.        */
  89.       waitUpLoad: [],
  90.       /**
  91.        * @Description
  92.        * 代码注释说明
  93.        * 未上传个数
  94.        * @Return
  95.        */
  96.       waitNum: NaN,
  97.       /**
  98.        * @Description
  99.        * 代码注释说明
  100.        * 上传大小限制
  101.        * @Return
  102.        */
  103.       limitFileSize: false,
  104.       /**
  105.        * @Description
  106.        * 代码注释说明
  107.        * 进度条
  108.        * @Return
  109.        */
  110.       percentage: 0,
  111.       percentageFlage: false,
  112.       /**
  113.        * @Description
  114.        * 代码注释说明
  115.        * 读取loading
  116.        * @Return
  117.        */
  118.       loading: false,
  119.       /**
  120.        * @Description
  121.        * 代码注释说明
  122.        * 正在上传中
  123.        * @Return
  124.        */
  125.       uploading: false,
  126.       /**
  127.        * @Description
  128.        * 代码注释说明
  129.        * 暂停上传
  130.        * @Return
  131.        */
  132.       stoped: false,

  133.       /**
  134.        * @Description
  135.        * 代码注释说明
  136.        * 上传后的文件数据
  137.        * @Return
  138.        */
  139.       fileData: {
  140.         id: "",
  141.         path: "",
  142.       },
  143.     };
  144.   },
  145.   /**
  146.    * 数据监听
  147.    * @type {Object}
  148.    */
  149.   watch: {
  150.     //监控上传进度
  151.     waitNum: {
  152.       handler(v, oldVal) {
  153.         let p = Math.floor(
  154.           ((this.formDataList.length - v) / this.formDataList.length) * 100
  155.         );
  156.         // debugger;
  157.         this.percentage = p > 100 ? 100 : p;
  158.       },
  159.       deep: true,
  160.     },
  161.     show: {
  162.       handler(v, oldVal) {
  163.         if (!v) {
  164.           this.file = null
  165.         }
  166.       },
  167.       deep: true,
  168.     },
  169.   },
  170.   /**
  171.    * 方法集合
  172.    * @type {Object}
  173.    */
  174.   methods: {
  175.     /**
  176.      * 代码注释说明
  177.      * 内存过滤器
  178.      * @param  {[type]} ram [description]
  179.      * @return {[type]}     [description]
  180.      */
  181.     ramFilter(bytes) {
  182.       if (bytes === 0) return "0";
  183.       var k = 1024;
  184.       let sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
  185.        let i = Math.floor(Math.log(bytes) / Math.log(k));
  186.       return (bytes / Math.pow(k, i)).toFixed(2) + " " + sizes[i];
  187.     },
  188.     /**
  189.      * 触发上传 文件处理
  190.      * @param e
  191.      */
  192.     async choseFile(e) {
  193.       const fileInput = e.target.files[0]; // 获取当前文件
  194.       this.imageSize = this.ramFilter(fileInput.size);//记录文件大小
  195.       if (!fileInput && !this.show) {
  196.         return;
  197.       }
  198.       const pattern = /[\u4e00-\u9fa5]/;
  199.       if (pattern.test(fileInput?.name)) {
  200.         this.$message.warning("请不要上传带有中文名称的镜像文件!");
  201.         return;
  202.       }

  203.       this.file = fileInput; // file 丢全局方便后面用 可以改进为func传参形式
  204.       this.percentage = 0;
  205.       if (this.file.size < this.maxFileSize) {
  206.         this.loading = true;
  207.         const FileSliceCap = this.sliceSize; // 分片字节数
  208.         let start = 0; // 定义分片开始切的地方
  209.         let end = 0; // 每片结束切的地方a
  210.         let i = 0; // 第几片
  211.         this.formDataList = []; // 分片存储的一个池子 丢全局
  212.         this.waitUpLoad = []; // 分片存储的一个池子 丢全局
  213.         while (end < this.file.size && this.show) {
  214.           /**
  215.            * @Description
  216.            * 代码注释说明
  217.            * 当结尾数字大于文件总size的时候 结束切片
  218.            * @Return
  219.            */
  220.           start = i * FileSliceCap; // 计算每片开始位置
  221.           end = (i + 1) * FileSliceCap; // 计算每片结束位置
  222.           var fileSlice = this.file.slice(start, end); // 开始切  file.slice 为 h5方法 对文件切片 参数为 起止字节数
  223.           const formData = new window.FormData(); // 创建FormData用于存储传给后端的信息
  224.           // formData.append('fileMd5', this.fileMd5) // 存储总文件的Md5 让后端知道自己是谁的切片
  225.           formData.append("file", fileSlice); // 当前的切片
  226.           formData.append("chunkNumber", i); // 当前是第几片
  227.           formData.append("fileName", this.file.name); // 当前文件的文件名 用于后端文件切片的命名  formData.appen 为 formData对象添加参数的方法
  228.           this.formDataList.push({ key: i, formData }); // 把当前切片信息 自己是第几片 存入我们方才准备好的池子
  229.           i++;
  230.         }
  231.         //获取文件的MD5值
  232.         this.computeFileMD5(this.file, FileSliceCap).then(
  233.           (res) => {
  234.             if (res) {
  235.               this.hash = res;
  236.               //console.log("拿到了:", res);
  237.               // this.UploadStatus = `文件读取成功(${res}),文件上传中...`;
  238.               //通过Md5值查询是否上传过
  239.               getImagecheckFile({ fileCode: res }).then(
  240.                 (res2) => {
  241.                   this.loading = false;
  242.                   /**
  243.                    * @Description
  244.                    * 代码注释说明
  245.                    * 全部切完以后 发一个请求给后端 拉当前文件后台存储的切片信息 用于检测有多少上传成功的切片
  246.                    * fileUrl:有地址就是秒传因为已经存在该文件了
  247.                    * shardingIndex:返回哪些已经上传用于断点续传
  248.                    * @Return
  249.                    */
  250.                   let { fileUrl, shardingIndex } = res2.data.data; //检测是否上传过
  251.                   if (!fileUrl) {
  252.                     /**
  253.                      * @Description
  254.                      * 代码注释说明
  255.                      * 当是断点续传时候
  256.                      * 记得处理一下当前是默认全都没上传过暂不支持断点续传后端无法返回已上传数据如果你家后端牛一点可以在此处理断点续传
  257.                      * @Return
  258.                      */
  259.                     this.waitUpLoad = this.formDataList;//当前是默认全都没上传过断点续传需处理
  260.                     this.getFile()
  261.                   } else {
  262.                     // debugger;
  263.                     this.formDataList = [{ key: fileUrl }];
  264.                     this.waitNum = 1;
  265.                     this.waitUpLoad = []; // 秒传则没有需要上传的切片
  266.                     this.$message.success("文件已秒传");
  267.                     this.$emit("fileinput", {
  268.                       url: fileUrl,
  269.                       code: this.hash,
  270.                       imageSize: this.imageSize,
  271.                     });
  272.                     this.waitNum = 0;
  273.                     // this.file = null;
  274.                     this.$refs.fileValue.value = ''
  275.                     this.uploading = false;
  276.                     this.loading = false;
  277.                     return;
  278.                   }
  279.                   this.waitNum = this.waitUpLoad.length; // 记录长度用于百分比展示
  280.                 },
  281.                 (err) => {
  282.                   this.$message.error("获取文件数据失败");
  283.                   this.file = null;
  284.                   this.$refs.fileValue.value = ''
  285.                   this.uploading = false;
  286.                   this.loading = false;
  287.                   return;
  288.                 }
  289.               );
  290.             } else {
  291.               // this.UploadStatus = "文件读取失败";
  292.             }
  293.           },
  294.           (err) => {
  295.             // this.UploadStatus = "文件读取失败";
  296.             this.uploading = false;
  297.             this.loading = false;
  298.             this.$message.error("文件读取失败");
  299.           }
  300.         );
  301.       } else {
  302.         //this.limitFileSize = true;
  303.         this.$message.error("请上传小于100G的文件");
  304.         this.file = null;
  305.         this.$refs.fileValue.value = ''
  306.         this.uploading = false;
  307.       }
  308.     },
  309.     //准备上传
  310.     getFile() {
  311.       /**
  312.        * @Description
  313.        * 代码注释说明
  314.        * 确定按钮
  315.        * @Return
  316.        */
  317.       if (this.file === null) {
  318.         this.$message.error("请先上传文件");
  319.         return;
  320.       }
  321.       this.percentageFlage = this.percentage == 100;
  322.       this.sliceFile(); // 上传切片
  323.     },
  324.     async sliceFile() {
  325.       /**
  326.        * @Description
  327.        * 代码注释说明
  328.        * 如果已上传文件且生成了文件路径
  329.        * @Return
  330.        */
  331.       if (this.fileData.path) {
  332.         return;
  333.       }
  334.       /**
  335.        * @Description
  336.        * 代码注释说明
  337.        * 如果是切片已全部上传 但还未完成合并及移除chunk操作 没有生成文件路径时
  338.        * @Return
  339.        */
  340.       if (this.percentageFlage && !this.fileData.path) {
  341.         this.finishUpload();
  342.         return;
  343.       }
  344.       this.uploading = true;
  345.       this.stoped = false;
  346.       //提交切片
  347.       this.upLoadFileSlice();
  348.     },
  349.     async upLoadFileSlice() {
  350.       if (this.stoped) {
  351.         this.uploading = false;
  352.         return;
  353.       }
  354.       /**
  355.        * @Description
  356.        * 代码注释说明
  357.        * 剩余切片数为0时调用结束上传接口
  358.        * @Return
  359.        */
  360.       try {
  361.         let suffix = /\.([0-9A-z]+)$/.exec(this.file.name)[1]; // 文件后缀名也就是文件类型
  362.         let data = {
  363.           bucketName: "static",//桶的名字
  364.           contentType: this.file.type || suffix,//文件类型
  365.           filename: this.file.name,//文件名字
  366.           partCount: this.waitUpLoad.length,//分片多少也就是分了多少个
  367.         };
  368.         //根据分片长度获取分片上传地址以及上传ID和文件地址
  369.         Imageinit(data).then((res) => {
  370.           if (res.data.code == 200 && res.data.data) {
  371.             this.uploadId = res.data.data.uploadId;//文件对应的id
  372.             this.fullPath = res.data.data.fullPath;//上传合并的地址
  373.             this.uploadUrls = res.data.data.uploadUrls;//每个分片对应的位置
  374.             if (this.uploadUrls && this.uploadUrls.length) {
  375.               /**
  376.                * 用于并发上传 parallelRun
  377.                */
  378.               // this.waitUpLoad.forEach((item, i) => {
  379.               //   item.formData.append("Upurl", this.uploadUrls[i]);
  380.               // });
  381.               // this.parallelRun(this.waitUpLoad)

  382.               // return;
  383.               let i = 0;//第几个分片对应地址
  384.               /**
  385.                * 文件分片合并
  386.                */
  387.               const complete = () => {
  388.                 Imagecomplete({
  389.                   bucketName: "static",//MinIO桶名称
  390.                   fullPath: this.fullPath,//Imageinit返回的上传地址
  391.                   uploadId: this.uploadId,//Imageinit返回的上传id
  392.                 }).then(
  393.                   (res) => {
  394.                     if (res.data.data) {
  395.                       this.uploading = false;
  396.                       this.$emit("fileinput", {
  397.                         url: "/*minIo桶地址*/" + this.fullPath,//最终文件路径表单提交用
  398.                         code: this.hash,//md5值校验
  399.                         imageSize: this.imageSize,//文件大小
  400.                         name: this.file.name,//文件名
  401.                       });
  402.                       this.$message({
  403.                         type: "success",
  404.                         message: "上传镜像成功",
  405.                       });
  406.                       this.$refs.fileValue.value = ''
  407.                       this.uploading = false;
  408.                     } else {
  409.                       this.file = null;
  410.                       this.$refs.fileValue.value = ''
  411.                       this.uploading = false;
  412.                       this.$message.error("合并失败");
  413.                     }
  414.                   },
  415.                   (err) => {
  416.                     this.file = null;
  417.                     this.$refs.fileValue.value = ''
  418.                     this.uploading = false;
  419.                     this.$message.error("合并失败");
  420.                   }
  421.                 );
  422.               };
  423.               /**
  424.                * 分片上传
  425.                */
  426.               const send = async () => {
  427.                 if (!this.show) {
  428.                   this.file = null;
  429.                   this.$refs.fileValue.value = ''
  430.                   this.uploading = false;
  431.                   return;
  432.                 }

  433.                 /**
  434.                  * 没有可上传的请求合并
  435.                  */
  436.                 if (i >= this.uploadUrls.length) {
  437.                   // alert('发送完毕')
  438.                   // 发送完毕
  439.                   complete();
  440.                   return;
  441.                 }
  442.                 if (this.waitNum == 0) return;

  443.                 /**
  444.                  * 通过AXIOS的put将对应的分片文件传到对应的桶里
  445.                  */
  446.                 try {
  447.                   axios
  448.                     .put(
  449.                       this.uploadUrls[i],
  450.                       this.waitUpLoad[i].formData.get("file")
  451.                     )
  452.                     .then(
  453.                       (result) => {
  454.                         /*上传一个分片成功就对应减少一个再接着下一个分片上传
  455.                         */
  456.                         this.waitNum--;
  457.                         i++;
  458.                         send();
  459.                       },
  460.                       (err) => {
  461.                         this.file = null;
  462.                         this.$refs.fileValue.value = ''
  463.                         this.uploading = false;
  464.                         this.$message.error("上传失败");
  465.                       }
  466.                     );
  467.                 } catch (error) {
  468.                   this.file = null;
  469.                   this.$refs.fileValue.value = ''
  470.                   this.uploading = false;
  471.                   this.$message.error("上传失败");
  472.                 }
  473.               };
  474.               send(); // 发送请求
  475.             }
  476.           }
  477.         });
  478.       } catch (error) {
  479.         this.file = null;
  480.         this.$refs.fileValue.value = ''
  481.         this.uploading = false;
  482.         this.$message.error("上传失败");
  483.       }
  484.     },
  485.    
  486.     inputChange() {
  487.       this.$refs["fileValue"].dispatchEvent(new MouseEvent("click"));
  488.     },   
  489.     /**
  490.      * 用于并发分片上传
  491.      * requestList 上传列表 max几个上传并发执行
  492.      */
  493.     async parallelRun(requestList, max = 10) {
  494.       const requestSliceList = [];
  495.       for (let i = 0; i < requestList.length; i += max) {
  496.         requestSliceList.push(requestList.slice(i, i + max));
  497.       }

  498.       for (let i = 0; i < requestSliceList.length; i++) {
  499.         const group = requestSliceList[i];
  500.         console.log(group);
  501.         try {

  502.           const res = await Promise.all(group.map(fn => axios.put(
  503.             fn.formData.get("Upurl"),
  504.             fn.formData.get("file")
  505.           )));
  506.           res.forEach(item => {
  507.             this.waitNum--
  508.           })
  509.           console.log('接口返回值为:', res);
  510.           if (this.waitNum === 0) {
  511.             //alert('发送完毕')

  512.             // 发送完毕
  513.             this.complete();
  514.             return;
  515.           }
  516.           // const res = await Promise.all(group.map(fn => fn));

  517.         } catch (error) {
  518.           console.error(error);
  519.         }
  520.       }

  521.     },
  522.     complete() {
  523.       Imagecomplete({
  524.         bucketName: "static",//对应的桶
  525.         fullPath: this.fullPath,//桶的地址
  526.         uploadId: this.uploadId,//桶的id
  527.       }).then(
  528.         (res) => {
  529.           if (res.data.data) {
  530.             this.uploading = false;
  531.             this.$emit("fileinput", {
  532.               url: "/*minIo桶地址*/" + this.fullPath,//'https://asgad/fileinput'+'/1000/20240701/xxx.zip'
  533.               code: this.hash,//文件MD5值
  534.               imageSize: this.imageSize,//文件大小
  535.             });
  536.             this.$message({
  537.               type: "success",
  538.               message: "上传镜像成功",
  539.             });
  540.             this.$refs.fileValue.value = ''
  541.             this.uploading = false;
  542.           } else {
  543.             this.file = null;
  544.             this.$refs.fileValue.value = ''
  545.             this.uploading = false;
  546.             this.$message.error("合并失败");
  547.           }
  548.         },
  549.         (err) => {
  550.           this.file = null;
  551.           this.$refs.fileValue.value = ''
  552.           this.uploading = false;
  553.           this.$message.error("合并失败");
  554.         }
  555.       );
  556.     },
  557.     /**
  558.      * 获取大文件的MD5数值
  559.      * @param {*} file 文件
  560.      * @param {*} n 分片大小单位M
  561.      */
  562.     computeFileMD5(file, n = 50 * 1024 * 1024) {
  563.       //("开始计算...", file);
  564.       return new Promise((resolve, reject) => {
  565.         let blobSlice =
  566.           File.prototype.slice ||
  567.           File.prototype.mozSlice ||
  568.           File.prototype.webkitSlice;
  569.         let chunkSize = n; // 默认按照一片 50MB 分片
  570.         let chunks = Math.ceil(file.size / chunkSize); // 片数
  571.         let currentChunk = 0;
  572.         let spark = new SparkMD5.ArrayBuffer();
  573.         let fileReader = new FileReader();
  574.         let that = this;
  575.         fileReader.onload = function (e) {
  576.           //console.log("read chunk nr", currentChunk + 1, "of", chunks);
  577.           spark.append(e.target.result);
  578.           currentChunk++;
  579.           // console.log("执行进度:" + (currentChunk / chunks) * 100 + "%");
  580.           if (currentChunk < chunks && that.show) {
  581.             loadNext();
  582.           } else {
  583.             // console.log("finished loading");

  584.             let md5 = spark.end(); //最终md5值
  585.             spark.destroy(); //释放缓存
  586.             if (currentChunk === chunks) {
  587.               resolve(md5);
  588.             } else {
  589.               reject(e);
  590.             }
  591.           }
  592.         };

  593.         fileReader.onerror = function (e) {
  594.           reject(e);
  595.         };

  596.         function loadNext() {
  597.           let start = currentChunk * chunkSize;
  598.           let end =
  599.             start + chunkSize >= file.size ? file.size : start + chunkSize;
  600.           fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
  601.         }

  602.         loadNext();
  603.       });
  604.     },
  605.   },
  606. };
  607. </script>
复制代码
页面样式

自行修改
  1. <!-- 当前组件页面样式定义 -->
  2. <style lang="scss" scoped>
  3. .file-choose-btn {
  4.   overflow: hidden;
  5.   position: relative;

  6.   input {
  7.     position: absolute;
  8.     font-size: 100px;
  9.     right: 0;
  10.     top: 0;
  11.     opacity: 0;
  12.     cursor: pointer;
  13.   }
  14. }

  15. .tips {
  16.   margin-top: 30px;
  17.   font-size: 14px;
  18.   font-weight: 400;
  19.   color: #606266;
  20. }

  21. .file-list {
  22.   margin-top: 10px;
  23. }

  24. .list-item {
  25.   display: block;
  26.   margin-right: 10px;
  27.   color: #606266;
  28.   line-height: 25px;
  29.   margin-bottom: 5px;
  30.   width: 90%;

  31.   .percentage {
  32.     float: right;
  33.   }
  34. }

  35. .list-enter-active,
  36. .list-leave-active {
  37.   transition: all 1s;
  38. }

  39. .list-enter,
  40. .list-leave-to

  41. /* .list-leave-active for below version 2.1.8 */
  42.   {
  43.   opacity: 0;
  44.   transform: translateY(-30px);
  45. }
  46. </style>
复制代码
总结

到此这篇关于前端大文件分片MinIO上传的文章就介绍到这了,更多相关前端大文件分片MinIO上传内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

来源:https://www.jb51.net/javascript/326821rfo.htm
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

举报 回复 使用道具