Element UI el-upload 多文件上传踩坑与解决方案

5 阅读5分钟

Element UI el-upload 多文件上传踩坑与解决方案

基于 Element UI 2.x 的 el-upload 组件,在使用 multiple 多文件上传时遇到的常见问题及解决方案。


坑一:on-success 中 fileUrl 为 undefined

现象

多文件上传后,fileList 中部分文件的 fileUrlundefined

[
  {
    "fileName": "mistake.xls",
    "name": "mistake.xls",
    "fileUrl": undefined,  // ❌ 缺失
    "url": undefined       // ❌ 缺失
  },
  {
    "fileName": "项目信息表.xlsx",
    "name": "项目信息表.xlsx",
    "fileUrl": "/营销管理/.../项目信息表.xlsx",  // ✅ 正常
    "url": "/营销管理/.../项目信息表.xlsx"        // ✅ 正常
  }
]

原因

on-success 回调是每个文件分别触发的,但回调参数中的 fileList 是 el-upload 内部维护的当前所有文件列表(包括还在上传中的文件)。

典型错误写法——先清空再全量重建

handleSuccess(response, file, fileList, form, item, type) {
  // ❌ 先清空
  this.attachmentList = [];

  if (response.code === 200) {
    // ❌ 遍历整个 fileList,包括还在上传中的文件
    fileList.forEach(i => {
      let pushFile = {
        fileName: i.name,
        // 还在上传中的文件:i.response 为 undefined,i.url 也为 undefined
        fileUrl: i.response ? i.response.data[0] : i.url,  // → undefined
        url: i.response ? i.response.data[0] : i.url,       // → undefined
      };
      this.attachmentList.push(pushFile);
    });
  }
}

当文件 B 先于文件 A 上传完成时,B 的 on-success 触发,此时遍历 fileList

  • 文件 A:还在上传中,i.responseundefinedi.url 也为 undefinedfileUrl = undefined
  • 文件 B:刚上传成功,i.response 存在 → fileUrl 正常

解决方案

增量更新:只处理本次上传成功的文件(response + file 参数),不遍历整个 fileList

handleSuccess(response, file, fileList, form, item, type) {
  if (response.code === 200) {
    // ✅ 只处理本次上传成功的文件
    const fileUrl = response.data[0];
    let pushFile = {
      fileName: file.name,
      name: file.name,
      fileUrl: fileUrl,
      url: fileUrl,
      item: this.getFileType(type),
    };
    this.attachmentDataList.push(pushFile);
    this.form.attachment = this.attachmentDataList;
  }
}

坑二:on-success 只触发一次(后续文件回调中断)

现象

同时选择多个文件上传,on-success 只触发了第一个文件的回调,后续文件的回调不再触发。

原因

on-success 回调中直接修改了绑定给 :file-list 的数组(如 push、重新赋值),会触发 el-upload 组件重新渲染,从而中断后续文件的钩子执行

// ❌ 在 on-success 中修改 :file-list 绑定的数组
this.attachmentList.push(pushFile);  // 触发组件重新渲染,中断后续 on-success

解决方案

职责分离:用一个独立的数据数组收集上传结果,不绑定给 :file-list

data() {
  return {
    attachmentList: [],     // 仅绑定 :file-list,用于展示回显,不在 on-success 中修改
    attachmentDataList: [], // 独立收集上传结果数据,供表单提交使用
  };
}
<!-- :file-list 只绑定展示用的数组 -->
<el-upload :file-list="attachmentList" ...>
handleSuccess(response, file, fileList, form, item, type) {
  if (response.code === 200) {
    const fileUrl = response.data[0];
    let pushFile = {
      fileName: file.name,
      name: file.name,
      fileUrl: fileUrl,
      url: fileUrl,
    };
    // ✅ 只操作独立的数据数组,不碰 :file-list 绑定的数组
    this.attachmentDataList.push(pushFile);
    this.form.attachment = this.attachmentDataList;
  }
}

提交表单时使用 attachmentDataList

submitForm() {
  // ✅ 用独立数据数组提交
  addTender({
    ...this.form,
    attachment: JSON.stringify(this.attachmentDataList.map(item => item.url))
  });
}

坑三:on-remove 中 fileUrl 为 undefined

现象

删除文件后,重建的列表中其他还在上传中的文件 fileUrlundefined

原因

与坑一类似,on-remove 回调中的 fileList 也包含还在上传中的文件,遍历时对无 response 且无 url 的文件赋值 undefined

解决方案

handleRemove 中遍历 fileList 重建时,跳过没有 fileUrl 的文件

handleRemove(file, fileList, type) {
  this.attachmentList = [];
  this.attachmentDataList = [];

  fileList.forEach(i => {
    let pushFile = {
      fileName: i.name,
      name: i.name,
      fileUrl: i.response ? i.response.data[0] : i.url,
      url: i.response ? i.response.data[0] : i.url,
    };
    // ✅ 跳过还在上传中没有 url 的文件
    if (!pushFile.fileUrl) return;
    this.attachmentList.push(pushFile);
    this.attachmentDataList.push(pushFile);
  });

  this.form.attachment = this.attachmentDataList;
}

坑四:编辑回显时数据不同步

现象

打开编辑弹窗时,已有附件能正常回显,但提交表单时数据为空。

原因

回显数据只写入了 attachmentList(绑定 :file-list),没有同步到 attachmentDataList(提交用的数据源)。

解决方案

打开编辑弹窗时,将回显数据同步到独立数据数组:

handleAdd() {
  this.reset();
  // ...其他初始化逻辑

  // ✅ 将已有的回显附件数据同步到 attachmentDataList
  this.attachmentDataList = JSON.parse(JSON.stringify(this.attachmentList));
  this.form.attachment = this.attachmentDataList;
}

最佳实践总结

核心原则:展示与数据分离

数组职责操作时机
attachmentList绑定 :file-list,仅负责展示回显初始化回显、on-remove 重建
attachmentDataList独立收集上传结果,供表单提交on-success 增量 push、on-remove 重建、编辑回显同步

on-success 原则

  1. 不要遍历 fileList——它包含还在上传中的文件
  2. 不要修改 :file-list 绑定的数组——会中断后续回调
  3. 只处理本次成功的文件——用 response + file 参数增量 push

on-remove 原则

  1. 可以遍历 fileList 重建,但要跳过 fileUrl 为空的文件
  2. 同时维护展示数组和数据数组

完整代码示例

data() {
  return {
    attachmentList: [],      // :file-list 展示用
    attachmentDataList: [],  // 提交数据用
  };
},

methods: {
  // 上传成功——增量更新
  handleSuccess(response, file, fileList, form, item, type) {
    if (response.code === 200) {
      const fileUrl = response.data[0];
      this.attachmentDataList.push({
        fileName: file.name,
        name: file.name,
        fileUrl: fileUrl,
        url: fileUrl,
      });
      this.form.attachment = this.attachmentDataList;
    }
  },

  // 删除文件——全量重建(跳过未完成的文件)
  handleRemove(file, fileList, type) {
    this.attachmentList = [];
    this.attachmentDataList = [];

    fileList.forEach(i => {
      const fileUrl = i.response ? i.response.data[0] : i.url;
      if (!fileUrl) return;  // 跳过还在上传中的文件
      const item = {
        fileName: i.name,
        name: i.name,
        fileUrl: fileUrl,
        url: fileUrl,
      };
      this.attachmentList.push(item);
      this.attachmentDataList.push(item);
    });

    this.form.attachment = this.attachmentDataList;
  },

  // 编辑回显——同步数据
  handleAdd() {
    this.reset();
    // 同步回显数据到提交数据源
    this.attachmentDataList = JSON.parse(JSON.stringify(this.attachmentList));
    this.form.attachment = this.attachmentDataList;
  },

  // 提交表单——使用独立数据数组
  submitForm() {
    addTender({
      ...this.form,
      attachment: JSON.stringify(this.attachmentDataList.map(item => item.url))
    });
  },
}

附录:el-upload 多文件上传回调执行顺序

用户选择文件 AB、C
        │
        ▼
  ┌─ A 开始上传 ──→ A on-success 触发(fileList 包含 AB⏳ C⏳)
  │
  ├─ B 开始上传 ──→ B on-success 触发(fileList 包含 AB✅ C⏳)
  │                  ↑ 注意:如果 A 先完成,此时 A 有 response
  │                    如果 C 先完成,此时 C 有 response
  │
  └─ C 开始上传 ──→ C on-success 触发(fileList 包含 AB✅ C✅)

  ⚠️ 上传完成顺序 ≠ 用户选择顺序,取决于网络和文件大小
  ⚠️ 每次回调的 fileList 都包含所有文件(含未完成的)

关键认知on-successfileList 参数是快照,不是"本次上传成功的文件列表"。每次回调都应该只关注 responsefile 这两个参数——它们才是本次成功的文件。