Vue 文件分片上传组件封装指南(记录)

引言

在现代 Web 应用中,大文件上传是一个常见需求。传统的文件上传方式在面对大文件时存在诸多问题:网络不稳定可能导致上传失败、服务器压力大、用户体验差等。本文将介绍如何使用 Vue 封装一个可复用的文件分片上传组件。

分片上传原理

分片上传的核心思想是将大文件分割成多个小块(chunks),然后逐个上传这些小块,最后在服务器端合并。

组件封装实现

1. 创建基础组件结构

1、首先创建一个 uploadminio.vue 组件,此组件用于上传文件:

<template>
  <el-dialog
    title="上传文件"
    :visible.sync="isMinioDialogVisible"
    width="60%"
    :before-close="handleClose"
    :modal-append-to-body="false"
    :append-to-body="true"
    v-el-drag-dialog
  >
    <div v-if="isFinite">
      <div>
        <input
          ref="fileInput"
          type="file"
          id="fileDoc"
          multiple
          @change="handleFileUpload"
          :accept="fileType"
          style="display: none"
        />
        <el-button
          class="otherZbtn"
          size="mini"
          type="primary"
          @click="triggerFileInput"
          :disabled="isminio"
          >选择文件</el-button
        >
      </div>
      <div>
        <el-table
          v-if="tableData.length > 0"
          size="mini"
          style="margin: 10px 0 15px"
          border
          header-row-class-name="tableHeader"
          :data="tableData"
          :highlight-current-row="true"
          :header-cell-style="{
            background: '#f7f8fa',
            color: 'rgb(3, 92, 160)',
            border: '1px solid #e8eaec',
          }"
        >
          <el-table-column
            type="index"
            header-align="center"
            align="center"
            label="序"
            width="50"
          />
          <el-table-column
            prop="name"
            header-align="center"
            align="center"
            label="文件名称"
          />
          <el-table-column
            prop="testItemName"
            header-align="center"
            align="center"
            label="进度"
            width="160"
          >
            <template slot-scope="scope">
              <el-tag v-if="scope.row.percentage == 100" size="mini"
                >上传完成</el-tag
              >
              <el-tag v-if="!scope.row.percentage" size="mini">暂未上传</el-tag>
              <el-progress
                v-if="scope.row.percentage != 0 && scope.row.percentage != 100"
                :percentage="scope.row.percentage"
              ></el-progress>
            </template>
          </el-table-column>
          <el-table-column
            prop="name"
            header-align="center"
            align="center"
            label="操作"
            width="80"
          >
            <template slot-scope="scope">
              <!-- 修改部分,添加点击事件 -->
              <el-button
                size="mini"
                style="color: red; cursor: pointer"
                @click="deleteFile(scope.$index)"
                :disabled="isminio"
                >删除</el-button
              >
            </template>
          </el-table-column>
        </el-table>
      </div>
      <span
        slot="footer"
        class="dialog-footer"
        style="display: flex; justify-content: center"
        v-loading="isminio"
      >
        <el-button @click="confirmClearFiles" size="small" type="danger"
          >清 空</el-button
        >
        <el-button type="primary" size="small" @click="uploadFiles"
          >上 传</el-button
        >
      </span>
    </div>
  </el-dialog>
</template>


<script>
import {
  uploadMerge,
  uploadInit,
  uploadChunk,
} from "../../api/upload/index.js";
// wenjianlist: {
//   fileList: [], // 文件列表
//   limitFlie: 3, // 上传文件个数
//   isNull: true, // 文件是否可以为空 false 是 true 否
//   fileNames: "", // 文件名称
//   fileUrls: "", //文件地址
//   fileUniques: "", //文件id
//   isMultiple: false, //是否批量上传 true 多 false 单 针对接口,不针对页面
//   isLook: true, // 查看or上传 true 查看
//   fileType:".png,.jpg,.xls,.doc",//允许上传的文件类型
//   fileMsg:"请上传",//为空后提示的话
//   isDown:1,// 是否可以下载  1可以  其他不可以
//   arrType: 1, // 1 新增文件 0 修改文件
// }
export default {
  name: "uploadMinio",
  data() {
    return {
      files: [], // 存储文件的数组
      CHUNK_SIZE: 5 * 1024 * 1024, // 5MB
      concurrentLimit: 3, // 并发上传数
      progress: {},
      isminio: false, // 控制loading
      isMinioDialogVisible: false, // 控制弹窗的显示
      allUploadSucceeded: false, // 所有文件上传是否成功
      tableData: [], // 表格数据
      tableFileData: [], // 表格文件数据
      MAX_FILE_COUNT: 99, // 新增:最大文件数量限制
      isFinite: true, // 新增:控制文件选择框的显示
      fileType: "", //可以上传的文件类型
    };
  },
  // 在组件销毁前重置input值
  beforeDestroy() {
    if (this.$refs.fileInput) {
      this.$refs.fileInput.value = "";
    }
  },
  methods: {
    // 打开上传文件弹窗
    showDialog(wenjianlist) {
      this.fileType = wenjianlist.fileType; // 文件类型
      this.MAX_FILE_COUNT = wenjianlist.limitFlie; // 传入的 limitFlie 赋值给 MAX_FILE_COUNT
      // 调用 clearFiles 方法清空选中的值
      this.clearFiles();
      this.isMinioDialogVisible = true;
    },
    // 上传文件
    triggerFileInput() {
      // 先重置再触发
      this.$refs.fileInput.value = "";
      this.$refs.fileInput.click();
    },
    handleFileUpload(e) {
      // 将新选中的文件转换为数组
      const newFiles = Array.from(e.target.files);
      // 检查添加新文件后是否会超过最大文件数量限制

      if (this.files.length + newFiles.length > this.MAX_FILE_COUNT) {
        this.$message.warning(`最多只能上传 ${this.MAX_FILE_COUNT} 个文件。`);
        return;
      }
      // 将新文件追加到现有的 this.files 数组中
      this.files = [...this.files, ...newFiles];
      // 更新表格数据,将新文件的信息也添加进去
      this.tableData = this.files.map((file) => ({
        name: file.name,
        percentage: 0,
      }));
    },
    handleClose(done) {
      // 清空 tableFileData 数组
      this.tableFileData = [];
      // 这里可以添加一些在关闭对话框前需要执行的逻辑,比如确认提示等
      // 最后调用 done() 来关闭对话框
      done();
    },
    async uploadFiles() {
      this.$emit("wenjianlistBack", this.tableFileData);
      // 新增:检查文件数组是否为空
      if (this.files.length === 0) {
        this.$message.warning("没有选择要上传的文件,请先选择文件。");
        return;
      }
      this.isminio = true;
      this.allUploadSucceeded = true;
      // 可以最后一个文件走完就关闭弹窗,现在是只要文件合并失败就不能关闭弹窗
      for (const file of this.files) {
        try {
          const uploadId = await this.initUpload(file.name);
          let uploadFileChunks = await this.uploadFileChunks(
            file,
            uploadId,
            file.name,
            file.size
          );
          if (uploadFileChunks) {
            await this.mergeChunks(uploadId, file.name);
          }
        } catch (error) {
          // 接口调用失败,将 isminio 设置为 false
          this.allUploadSucceeded = false;
          this.isminio = false;
          console.error("文件上传失败:", error);
        }
      }
      if (this.allUploadSucceeded) {
        // 所有文件上传成功,传到父组件,父组件在传到显示组件
        this.$emit("wenjianlistBack", this.tableFileData);
        //关闭弹窗
        this.isMinioDialogVisible = false;
      }
      this.isminio = false;
    },

    async initUpload(fileName) {
      try {
        const res = await uploadInit();
        if (res && res.code == 200) {
          return res.uploadId;
        }
        throw new Error("上传初始化失败");
      } catch (err) {
        // 接口调用失败,将 isminio 设置为 false
        this.isminio = false;
        console.log(err);
        throw err;
      }
    },

    async uploadFileChunks(file, uploadId, fileName, fileSize) {
      let isTrue = true;
      const totalChunks = Math.ceil(file.size / this.CHUNK_SIZE);
      const chunks = Array.from({ length: totalChunks }, (_, i) => i);

      // 控制并发数
      const uploadQueue = [];
      let vali = 0;
      for (let i = 0; i < chunks.length; i++) {
        const chunk = file.slice(
          i * this.CHUNK_SIZE,
          (i + 1) * this.CHUNK_SIZE
        );
        const formData = new FormData();
        formData.append("uploadId", uploadId);
        formData.append("chunkIndex", i);
        formData.append("chunk", chunk);
        formData.append("fileName", fileName);
        formData.append("fileSize", fileSize);

        // 先找到目标文件
        const targetFile = this.tableData.find(
          (item) => item.name === fileName
        );
        // 启动一个定时器让进度条缓慢增长
        const intervalId = setInterval(() => {
          if (targetFile.percentage < ((vali + 1) / totalChunks) * 99) {
            targetFile.percentage++;
          }
        }, 500); // 每500毫秒增长1%

        try {
          uploadQueue.push(
            // 上传文件
            uploadChunk(formData)
              .then((res) => {
                // 清除定时器
                clearInterval(intervalId);
                if (res && res.code == 200) {
                  //上传分片成功后,更新进度条到正确数值
                  targetFile.percentage = Math.round(
                    ((vali + 1) / totalChunks) * 100
                  );
                  vali++;
                  isTrue = true;
                } else {
                  isTrue = false;
                }
              })
              .catch((err) => {
                isTrue = false;
                console.log("err", err);
              })
          );
        } catch (err) {
          // 清除定时器
          clearInterval(intervalId);
          // 接口调用失败,将 isminio 设置为 false
          this.isminio = false;
          console.log(err);
          throw err;
        }
        // 控制并发
        if (uploadQueue.length >= this.concurrentLimit) {
          await Promise.all(uploadQueue);
          uploadQueue.length = 0;
        }
      }
      await Promise.all(uploadQueue);
      return isTrue;
    },

    async mergeChunks(uploadId, fileName) {
      let params = new FormData();
      params.append("uploadId", uploadId);
      params.append("fileName", fileName);
      const targetFile = this.tableData.find((item) => item.name === fileName);
      try {
        const res = await uploadMerge(params);
        if (res && res.code == 200) {
          this.tableFileData.push(res.map);
          console.log("文件上传成功");
          // 修改部分:文件合并成功后,将进度条设置为 100%
          if (targetFile) {
            targetFile.percentage = 100;
          }
        } else {
          // 修改部分:文件合并失败后,将进度条设置为 0%
          if (targetFile) {
            targetFile.percentage = 0;
          }
          this.allUploadSucceeded = false;
        }
      } catch (err) {
        this.allUploadSucceeded = false;
        // 接口调用失败,将 isminio 设置为 false
        this.isminio = false;
        console.log(err);
        throw err;
      }
    },
    confirmClearFiles() {
      // 新增判断,如果文件数组为空,直接清空文件
      if (this.files.length === 0) {
        this.clearFiles();
        this.$message({
          type: "success",
          message: "文件已清空.!",
        });
        return;
      }
      this.$confirm("确定要清空所有上传文件吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          this.clearFiles();
          this.$message({
            type: "success",
            message: "文件已清空!",
          });
        })
        .catch(() => {
          this.$message({
            type: "info",
            message: "已取消清空操作",
          });
        });
    },
    clearFiles() {
      // 新增:清空文件列表前,先隐藏文件选择框
      this.isminio = false;
      this.isFinite = false; // 新增:隐藏文件选择框
      // 清空 tableFileData 数组
      this.tableFileData = [];
      // 清空文件列表
      this.files = [];
      // 清空表格数据
      this.tableData = [];
      // 清空进度条
      this.progress = {};
      setTimeout(() => {
        this.isFinite = true; // 新增:显示文件选择框
        // 清空文件输入框的选中文件
        const fileInput = document.getElementById("fileDoc");
        if (fileInput) {
          fileInput.value = "";
          fileInput.outerHTML = fileInput.outerHTML;
        }
      }, 0);
    },
    // 添加删除文件方法
    deleteFile(index) {
      // 从 files 数组中移除对应的文件
      this.files.splice(index, 1);
      // 从 tableData 数组中移除对应的文件信息
      this.tableData.splice(index, 1);
    },
  },
};
</script>

<style scoped>
/* 隐藏原来的上传列表 */
::v-deep .uploadFile .el-upload-list {
  display: none;
}
</style>

2、其次在创建一个 uploadminioLook.vue 组件,此组件用于显示已经上传成功的文件:

<template>
  <!-- 上传文件的显示列表 -->
  <div v-loading="isfileload">
    <el-table
      v-if="fileList.length"
      :data="fileList"
      :max-height="tableMaxHeight"
      style="width: 100%"
      size="mini"
      row-key="uid"
      @selection-change="selectionChangeHandle"
      @select-all="selectAll"
    >
      <el-table-column
        :reserve-selection="true"
        type="selection"
        header-align="center"
        align="center"
        width="50"
      ></el-table-column>
      <el-table-column prop="name" label="名称" />
      <el-table-column width="100" prop="address" fixed="right" label="操作">
        <template slot-scope="scope">
          <span
            title="下载"
            style="
              display: inline-block;
              margin-right: 10px;
              cursor: pointer;
              font-size: 14px;
            "
            @click="handlePreview(scope.row)"
            ><i class="el-icon-download"></i
          ></span>
          <span
            v-if="isLook"
            title="删除"
            style="display: inline-block; margin-right: 10px"
            @click="handleRemove(scope.row)"
            ><i class="el-icon-delete"></i
          ></span>
          <span
            title="查看图片"
            style="display: inline-block"
            @click="handlePictureCardPreview(scope.row)"
            v-if="isHzm(scope.row)"
            ><i class="el-icon-view"></i
          ></span>
          <span
            title="查看pdf"
            style="display: inline-block"
            @click="handlePdfPreview(scope.row)"
            v-if="isPdf(scope.row)"
            ><i class="el-icon-view"></i
          ></span>
        </template>
      </el-table-column>
    </el-table>
    <el-dialog
      :visible.sync="picVisible"
      title="文件详情"
      width="70%"
      :modal-append-to-body="false"
      :append-to-body="true"
      v-el-drag-dialog
    >
      <div style="width: 100; height: 100%; overflow: auto">
        <img
          style="max-width: 100%; margin: 15px auto"
          :src="dialogImageUrl"
          alt=""
          v-if="dialogImageUrl != ''"
        />
        <iframe
          :src="'pdf/web/viewer.html?file=' + pdfSrc"
          frameborder="0"
          width="100%"
          :height="tableMaxHeight"
          v-if="pdfSrc != ''"
        ></iframe>
      </div>
    </el-dialog>
    <div style="height: 1px; overflow: hidden">
      <iframe :src="urllist" frameborder="0" width="100" height="100"></iframe>
    </div>
  </div>
</template>

<script>
import { uploadFile, deleteFile } from "../../api/upload/index.js";
// wenjianlist: {
//   fileList: [], // 文件列表
//   limitFlie: 3, // 上传文件个数
//   isNull: true, // 文件是否可以为空 false 是 true 否
//   fileNames: "", // 文件名称
//   fileUrls: "", //文件地址
//   fileUniques: "", //文件id
//   isMultiple: false, //是否批量上传 true 多 false 单 针对接口,不针对页面
//   isLook: true, // 查看or上传 true 查看
//   fileType:".png,.jpg,.xls,.doc",//允许上传的文件类型
//   fileMsg:"请上传",//为空后提示的话
//   isDown:1,// 是否可以下载  1可以  其他不可以
//   arrType: 1, // 1 新增文件 0 修改文件
// }
export default {
  name: "uploadminioLook",
  data() {
    return {
      picVisible: false, // 文件详情弹窗
      dialogImageUrl: "", // 图片路径
      pdfSrc: "", // pdf路径
      isfileload: false, // 上传文件组件显示隐藏
      dataForm: {
        fileNames: "",
        fileUrls: "",
      },
      limitFlie: 1, //上传文件个数
      isNull: true, // 文件是否可以为空
      isMultiple: false, //单文件上传 or 多文件上传
      isLook: false,
      fileDateArr: [], //附件删除列表
      fileIdArr: [], //附件删除id列表
      urllist: "",
      timmer: null, // 用于控制防抖的定时器
      fileList: [], // 文件列表--上传结束后回显用
      file: [],
      disabled: false, // 控制组件是否禁用
      uploadFileList: [], // 上传文件列表--待传递
      successFileUrls: [], // 记录上传成功的文件列表--用不到,这里为了方便演示
      fileType: "", // 上传文件类型
      fileMsg: "", //为空后提示的话
      isDown: 1, // 文件是否可以下载
      dataListSelections: [], //多选
    };
  },
  props: {
    wenjianlist: {
      type: Object,
      default: () => {},
    },
  },
  computed: {
    tableMaxHeight() {
      return window.innerHeight - 250 + "px";
    },
    isHzm() {
      return function (value) {
        if (value) {
          let name = value.filePath;
          let val = name.substring(name.lastIndexOf(".")).toLowerCase();
          if (!/\.(jpg|jpeg|png|GIF)$/.test(val)) {
            return false;
          } else {
            return true;
          }
        }
      };
    },
    isPdf() {
      return function (value) {
        if (value) {
          let name = value.filePath;
          let val = name.substring(name.lastIndexOf(".")).toLowerCase();
          if (val == ".pdf") {
            return true;
          } else {
            return false;
          }
        }
      };
    },
    iswordexcal() {
      return function (value) {
        if (value) {
          let name = value.filePath;
          let val = name.substring(name.lastIndexOf(".")).toLowerCase();
          if (
            val == ".docx" ||
            val == ".doc" ||
            val == ".xlsx" ||
            val == ".xls"
          ) {
            return true;
          } else {
            return false;
          }
        }
      };
    },
  },
  watch: {
    wenjianlist: {
      handler(newVal, oldVal) {
        this.urllist = "";
        this.file = [];
        this.limitFlie = newVal.limitFlie;
        this.fileType = newVal.fileType;
        this.fileMsg = newVal.fileMsg;
        // 文件是否可以下载
        if (newVal.isDown) {
          this.isDown = newVal.isDown;
        }

        this.isNull = newVal.isNull;
        this.isMultiple = newVal.isMultiple;
        this.isLook = newVal.isLook;
        if (newVal.arrType == 1) {
          // 回显的文件列表
          let name = newVal.fileNames ? newVal.fileNames.split(",") : [];
          let filePath = newVal.fileUrls ? newVal.fileUrls.split(",") : [];
          let id = newVal.fileUniques ? newVal.fileUniques.split(",") : [];
          let fileList = [];
          if (name.length && name[0] != "") {
            for (let i in name) {
              fileList.push({
                name: name[i],
                filePath: filePath[i],
                id: id[i],
              });
            }
          }
          this.fileList = fileList;
        } else {
          // 重新上传的文件
          this.fileList = newVal.fileList;
        }
      },
      deep: true,
      immediate: true,
    },
  },
  created() {},
  methods: {
    // 提交成功之后清空数
    serNull() {
      this.fileList = []; //附件列表
      this.fileDateArr = []; //附件删除列表
      this.fileIdArr = []; //附件删除id列表
      this.uploadFileList = [];
      this.file = [];
    },
    // 多选
    selectionChangeHandle(val) {
      this.dataListSelections = val;
    },
    selectAll(val) {
      this.dataListSelections = val;
    },
    // 批量下载
    allDown() {
      console.log(this.dataListSelections);
    },
    //查看大图
    async handlePictureCardPreview(file) {
      this.pdfSrc = "";
      this.dialogImageUrl = "";
      this.isfileload = true;
      this.dialogImageUrl = await this.fileLook(file.filePath);
      this.picVisible = true;
      this.isfileload = false;
    },
    //查看pdf
    async handlePdfPreview(file) {
      this.pdfSrc = "";
      this.dialogImageUrl = "";
      this.isfileload = true;
      this.pdfSrc = await this.filePdf(file.filePath);
      this.picVisible = true;
      this.isfileload = false;
    },
    // 点击文件下载
    async handlePreview(file) {
      if (this.isDown && this.isDown != 1) {
        this.$message.info("暂无下载权限");
        return false;
      }
      this.isfileload = true;
      await this.fileDown({
        url: file.filePath,
        name: file.name,
      });
      this.isfileload = false;
    },
    //删除文件
    async picRemove(picarr, picid) {
      for (let i = 0; i < picarr.length; i++) {
        if (picarr[i].id === picid) {
          picarr.splice(i, 1);
        }
      }
      return picarr;
    },
    async picRemove1(data) {
      deleteFile(data).then((res) => {
        if (res.code != 200) {
        } else {
        }
      });
    },
    handleRemove(file) {
      let data = new FormData();
      let dataminio = "";
      let filesid = "";
      filesid = file.id;
      data.append("filePath", file.filePath);
      dataminio = file.filePath;
      for (let i in this.fileList) {
        if (filesid == this.fileList[i].id) {
          this.fileList.splice(i, 1);
        }
      }
      this.fileDateArr.push(dataminio);
      this.fileIdArr.push(filesid);
      this.$message({
        message: "文件删除成功",
        type: "success",
        duration: 1000,
      });
    },
    //提交 true 文件可以为空  false 文件不能为空
    async submitupload() {
      if (this.fileDateArr) {
        for (let i in this.fileDateArr) {
          await this.picRemove(this.fileList, this.fileIdArr[i]);
        }
      }
      if (!this.isNull) {
        if (this.fileList.length) {
          if (!this.fileList.length) {
            this.$message({
              message: this.fileMsg ? this.fileMsg : "请上传文件",
              type: "error",
              duration: 1000,
            });
            return false;
          }
        } else {
          this.$message({
            message: this.fileMsg ? this.fileMsg : "请上传文件",
            type: "error",
            duration: 1000,
          });
          return false;
        }
      }
    },
  },
};
</script>

2. 在项目中使用组件

在需要使用的地方引入组件:
template

<el-col :span="24">
  <el-form-item label="附件:" prop="">
    <el-button
      @click="handlePic"
      class="otherZbtn"
      size="mini"
      type="primary"
      >上传文件</el-button
    >
    <uploadMinio
      ref="uploadMinioRef"
      @wenjianlistBack="wenjianlistBack"
    ></uploadMinio>
    <uploadminioLook
      ref="uploadminioLookRef"
      :wenjianlist="wenjianlist"
    ></uploadminioLook>
  </el-form-item>
</el-col>

js

import uploadMinio from "../../../components/uploads/uploadminio.vue";
import uploadminioLook from "../../../components/uploads/uploadminioLook.vue";
components: {
  uploadMinio,
  uploadminioLook,
},
data() {
    return {
      wenjianlist: {
        fileList: [],
        fileNames: "",
        fileUrls: "",
        fileUniques: "",
        limitFlie: 999, // 上传文件个数
        isNull: true, // 文件是否可以为空 false 是 true 否
        isMultiple: false,
        isLook: true,
        time: "",
        arrType: 1, // 1 新增 0 修改
     },
  }
}
 methods: {
	  // 上传文件************************************
    handlePic() {
      // 获取当前文件列表的长度
      const currentFileCount = this.$refs.uploadminioLookRef.fileList.length;
      // 获取允许上传的最大文件数量
      const limit = this.wenjianlist.limitFlie;
      // 检查当前文件数量是否超过限制
      if (currentFileCount >= limit) {
        this.$message.warning("已达到最大文件上传数量,无法继续上传。");
        return;
      }
      // 如果未超过限制,打开上传对话框
      this.$refs.uploadMinioRef.showDialog(this.wenjianlist);
    },
    // 文件回显
    wenjianlistBack(data, val) {
      console.log("data", data);
      if (val == 1) {
        this.wenjianlist.fileNames = data.fileNames;
        this.wenjianlist.fileUniques = data.fileUniques;
        this.wenjianlist.fileUrls = data.fileUrls;
        this.wenjianlist.arrType = 1;
        this.wenjianlist.time = new Date().getTime();
      } else {
        console.log("this.wenjianlist.fileList", this.wenjianlist.fileList);
        this.wenjianlist.arrType = 0;
        // 和已经存在的文件进行合并
        this.wenjianlist.fileList = [
          ...this.$refs.uploadminioLookRef.fileList,
          ...data,
        ];
        this.wenjianlist.time = new Date().getTime();
      }
    },
  // ************************************
  // ************************************
 // 附件提交
   await this.$refs.uploadminioLookRef.submitupload();
   let wjList = this.$refs.uploadminioLookRef.fileList; // 提交的文件
   let fileDateArr = this.$refs.uploadminioLookRef.fileDateArr; // 删除的文件
   console.log(wjList);
   console.log(fileDateArr);
   if (wjList.length) {
     let name = [];
     let url = [];
     let id = [];
     for (let i in wjList) {
       name.push(wjList[i].name);
       url.push(wjList[i].filePath);
       id.push(wjList[i].id);
     }
     this.dataForm.fileNames = name.join(",");
     this.dataForm.fileUrls = url.join(",");
     this.dataForm.fileUniques = id.join(",");
   } else {
     this.dataForm.fileNames = "";
     this.dataForm.fileUrls = "";
     this.dataForm.fileUniques = "";
     // 文件不能为空的情况
     if (!this.wenjianlist.isNull) {
       this.isdisabled = false; //打开提交按钮
       return false;
     }
   }
 // ************************************
// 删除文件 新增或者修改成功之后在删除需要删除的文件
 if (fileDateArr.length) {
   for (let i in fileDateArr) {
     await this.$refs.uploadminioLookRef.picRemove1(
       fileDateArr[i]
     );
   }
 }
}

服务器端实现要点

为了使分片上传正常工作,服务器端需要实现以下接口:

​​验证接口​​ (/api/init) - 检查文件是否已存在或部分存在
​​上传接口​​ (/api/chunk) - 接收文件分片并保存
​​合并接口​​ (/api/merge) - 将所有分片合并为完整文件

3.实现效果图如下

1、打开上传弹窗
在这里插入图片描述
2、选择文件
在这里插入图片描述

3、上传中
在这里插入图片描述

4、上传成功
在这里插入图片描述

优化与扩展

**​​并发控制​​:**限制同时上传的分片数量,避免浏览器性能问题
**​​错误重试​​:**为每个分片添加重试机制,提高上传成功率
**​​速度限制​​:**添加上传速度限制选项
**​​文件预览​​:**在上传前显示文件预览(如图片)

总结

通过封装 Vue 文件分片上传组件,我们实现了以下功能:

1 大文件分片上传,提高上传成功率;
2上传进度实时显示;
3文件验证避免重复上传;
4良好的用户反馈;

这种组件化实现方式可以方便地在不同项目中复用,只需根据实际需求调整服务器端接口即可。希望本文能帮助你理解并实现文件分片上传功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值