Element UI el-upload 多文件上传踩坑与解决方案
基于 Element UI 2.x 的
el-upload组件,在使用multiple多文件上传时遇到的常见问题及解决方案。
坑一:on-success 中 fileUrl 为 undefined
现象
多文件上传后,fileList 中部分文件的 fileUrl 为 undefined:
[
{
"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.response为undefined,i.url也为undefined→fileUrl = 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
现象
删除文件后,重建的列表中其他还在上传中的文件 fileUrl 为 undefined。
原因
与坑一类似,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 原则
- 不要遍历
fileList——它包含还在上传中的文件 - 不要修改
:file-list绑定的数组——会中断后续回调 - 只处理本次成功的文件——用
response+file参数增量 push
on-remove 原则
- 可以遍历
fileList重建,但要跳过fileUrl为空的文件 - 同时维护展示数组和数据数组
完整代码示例
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 多文件上传回调执行顺序
用户选择文件 A、B、C
│
▼
┌─ A 开始上传 ──→ A on-success 触发(fileList 包含 A✅ B⏳ C⏳)
│
├─ B 开始上传 ──→ B on-success 触发(fileList 包含 A✅ B✅ C⏳)
│ ↑ 注意:如果 A 先完成,此时 A 有 response
│ 如果 C 先完成,此时 C 有 response
│
└─ C 开始上传 ──→ C on-success 触发(fileList 包含 A✅ B✅ C✅)
⚠️ 上传完成顺序 ≠ 用户选择顺序,取决于网络和文件大小
⚠️ 每次回调的 fileList 都包含所有文件(含未完成的)
关键认知:on-success 的 fileList 参数是快照,不是"本次上传成功的文件列表"。每次回调都应该只关注 response 和 file 这两个参数——它们才是本次成功的文件。