news 2026/8/26 15:12:27

上传文件夹时有文件打开状态导致失败问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
上传文件夹时有文件打开状态导致失败问题

背景:

使用ElementPlus的<el-upload>的directory上传文件夹属性时,文件夹里的所有文件在没有打开的情况下是可以正常上传并且成功的,但如果其中的某个文件处于打开状态,上传就会出问题,接口报红如图一所示,Preview 如图二所示,帮我看一下这是什么问题,有什么好的解决思路和解决办法呢?


从你的描述和截图来看,问题很明确:

当使用<el-upload>directory属性上传文件夹时,如果文件夹内有正在被其他程序打开(占用)的文件(如正在被编辑的 Word/Excel、正在被预览的 PDF、正在被播放的视频等),上传请求就会失败,报net::ERR_FAILED


问题原因

这不是 ElementPlus 或浏览器的问题,而是操作系统文件系统的限制

  1. 文件被独占锁定:当某个程序以独占模式打开一个文件时(例如 Microsoft Office 正在编辑 .docx 文件),操作系统会阻止其他进程对该文件进行读取操作。

  2. 浏览器无法读取<input type="file" webkitdirectory>在获取文件列表时只能拿到文件元数据(文件名、大小等),但实际读取文件内容时,浏览器会尝试通过系统 API 读取文件流。如果文件被锁定,读取操作就会失败,导致 XHR 请求直接报ERR_FAILED

  3. 前端无法感知:浏览器 File API 在枚举文件时不会检查文件是否可读,只有在真正开始读取(上传)时才会暴露问题。


解决思路与方案

方案一:上传前预检 + 用户提示(推荐)

在开始上传之前,先尝试用FileReaderBlob.slice()预读每个文件的一小部分,检测文件是否可读。如果不可读,则提示用户关闭对应文件。

// 预检函数 async function checkFileReadable(file: File): Promise<boolean> { return new Promise((resolve) => { if (file.size === 0) { resolve(true) // 空文件跳过检测 return } const reader = new FileReader() reader.onload = () => resolve(true) reader.onerror = () => resolve(false) // 只读取前4字节,速度快 const blob = file.slice(0, 4) reader.readAsArrayBuffer(blob) }) } // 上传前调用 async function beforeUpload(files: File[]) { const unreadableFiles: string[] = [] for (const file of files) { const readable = await checkFileReadable(file) if (!readable) { unreadableFiles.push(file.name) } } if (unreadableFiles.length > 0) { ElMessage.error(`以下文件被其他程序占用,请关闭后重试:\n${unreadableFiles.join('\n')}`) return false } return true }

优点:用户体验好,能明确告知哪些文件有问题。
缺点:大文件夹预检会消耗一些时间(但只读4字节,影响很小)。


方案二:上传失败后跳过 + 记录日志

在上传过程中捕获错误,跳过失败的文件,继续上传其他文件,最后汇总报告失败列表。

// 使用自定义上传方法 const uploadFile = async (file: File): Promise<{ success: boolean; error?: string }> => { try { const formData = new FormData() formData.append('file', file) await axios.post('/api/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }) return { success: true } } catch (error) { return { success: false, error: `${file.name} 上传失败,可能文件被占用` } } } // 批量上传 async function uploadAll(files: File[]) { const results = [] for (const file of files) { const result = await uploadFile(file) results.push(result) if (!result.success) { // 记录失败但不中断 console.warn(result.error) } } // 最后汇总 const failed = results.filter(r => !r.success) if (failed.length > 0) { ElMessage.warning(`共 ${failed.length} 个文件上传失败,请检查是否被其他程序占用`) } }

优点:不影响其他文件的上传。
缺点:用户可能不知道哪些文件失败了,需要额外展示失败列表。


方案三:改用分片上传 + 重试机制

如果文件被短暂锁定(例如正在被复制或杀毒软件扫描),可以加入重试逻辑:

async function uploadWithRetry(file: File, maxRetries = 3): Promise<boolean> { for (let i = 0; i < maxRetries; i++) { try { const formData = new FormData() formData.append('file', file) await axios.post('/api/upload', formData) return true } catch (e) { if (i === maxRetries - 1) throw e // 等待后重试 await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))) } } return false }

优点:对瞬时锁定的文件有效。
缺点:如果文件一直被占用,重试也没用,还是要提示用户。


方案四(后端配合):后端处理文件流读取异常

如果前端无法解决,可以后端接收文件流时捕获读取异常,返回明确的错误码,前端据此展示友好提示。


总结建议

方案适用场景推荐度
方案一(预检)文件数量适中,用户体验优先⭐⭐⭐⭐⭐
方案二(跳过失败)文件数量多,允许部分失败⭐⭐⭐⭐
方案三(重试)文件被短暂占用⭐⭐⭐
方案四(后端配合)前端无法区分错误类型⭐⭐⭐

最推荐的组合方案一 + 方案二结合使用 —— 预检提示用户关闭占用文件,同时在上传过程中对个别突发的失败做记录和提示。


结合我现在写的代码,帮我整改一下


看了你的代码,问题很明显:你在input.onchange中直接把所有文件塞进一个FormData,然后一次性发送给后端。如果其中有任何一个文件被占用,整个请求就会失败。

下面是针对你现有代码的整改方案,采用方案一(预检)+ 方案二(跳过失败)的组合策略:


整改后的完整代码

// 工具函数:检查单个文件是否可读 const checkFileReadable = (file: File): Promise<boolean> => { return new Promise((resolve) => { if (file.size === 0) { resolve(true) // 空文件直接通过 return } const reader = new FileReader() reader.onload = () => resolve(true) reader.onerror = () => resolve(false) // 只读取前4字节,速度快且不会占用太多内存 const blob = file.slice(0, 4) reader.readAsArrayBuffer(blob) }) } // 预检所有文件,返回可读和不可读的文件列表 const preCheckFiles = async (files: File[]): Promise<{ readableFiles: File[] unreadableFiles: string[] }> => { const readableFiles: File[] = [] const unreadableFiles: string[] = [] // 使用 for 循环而不是 forEach,以便逐个等待 for (const file of files) { const isReadable = await checkFileReadable(file) if (isReadable) { readableFiles.push(file) } else { unreadableFiles.push(file.name) } } return { readableFiles, unreadableFiles } } // 触发文件夹上传 const triggerFolderUpload = () => { const input = folderUploadRef.value.$el.querySelector('input') as HTMLInputElement if (!input) return // 每次点击重置事件,防止重复绑定 input.onchange = async (e: Event) => { const target = e.target as HTMLInputElement const files = Array.from(target.files || []) if (!files.length) return cur.value.tableLoading = true try { // ===== 第一步:预检所有文件 ===== ElMessage.info(`正在检测 ${files.length} 个文件是否可读...`) const { readableFiles, unreadableFiles } = await preCheckFiles(files) // ===== 第二步:处理不可读文件 ===== if (unreadableFiles.length > 0) { // 用弹窗明确告知用户哪些文件被占用 const fileNameStr = unreadableFiles.slice(0, 10).join('\n') + (unreadableFiles.length > 10 ? `\n... 还有 ${unreadableFiles.length - 10} 个文件` : '') await ElMessageBox.confirm( `以下 ${unreadableFiles.length} 个文件正在被其他程序占用,请关闭后重新上传:\n\n${fileNameStr}\n\n是否跳过这些文件继续上传?`, '文件被占用', { confirmButtonText: '跳过并继续', cancelButtonText: '取消上传', type: 'warning', dangerouslyUseHTMLString: false } ).catch(() => { // 用户点击取消,终止上传 cur.value.tableLoading = false return }) // 如果用户确认跳过,但可读文件为0,则直接返回 if (readableFiles.length === 0) { ElMessage.warning('没有可上传的文件') cur.value.tableLoading = false return } } // ===== 第三步:只上传可读的文件 ===== if (readableFiles.length === 0) { ElMessage.warning('没有可上传的文件') cur.value.tableLoading = false return } ElMessage.info(`正在上传 ${readableFiles.length} 个文件${unreadableFiles.length > 0 ? `(已跳过 ${unreadableFiles.length} 个被占用的文件)` : ''}...`) // ===== 第四步:分片上传(改为逐个文件上传,避免一个失败导致全部失败) ===== const uploadResults = await uploadFilesSequentially(readableFiles) // ===== 第五步:处理上传结果 ===== const failedFiles = uploadResults.filter(r => !r.success) if (failedFiles.length > 0) { const failedNames = failedFiles.map(r => r.name).join('\n') ElMessage.warning(`上传完成,但有 ${failedFiles.length} 个文件上传失败(可能被占用或网络问题):\n${failedNames}`) } else { ElMessage.success(`成功上传 ${uploadResults.length} 个文件!`) } // ===== 第六步:解析文件夹名称 ===== const firstPath = files[0]?.webkitRelativePath || '' cur.value.folderName = firstPath.includes('/') ? firstPath.split('/')[0] : '所选文件夹' cur.value.fileTotal = uploadResults.filter(r => r.success).length // ===== 第七步:如果有成功上传的文件,调用接口 ===== const successFiles = uploadResults.filter(r => r.success).map(r => r.file) if (successFiles.length > 0) { const formData = new FormData() successFiles.forEach((file) => { formData.append('files', file) }) if (activeTab.value === 'employee') { const data = await GongShiAnalyzeApi(formData) getGongShiData(data) } else { const data = await QiHuaGongShiAnalyzeApi(formData) getGongShiData(data) } } } catch (error) { console.error('上传失败:', error) ElMessage.error('上传失败,请重试') cur.value.workHourList = [] cur.value.folderName = '' } finally { cur.value.tableLoading = false // 清空 input 的值,以便再次选择相同文件夹时能触发 change target.value = '' } } // 手动触发点击 input.click() } // 逐个上传文件(避免一个失败导致全部失败) const uploadFilesSequentially = async (files: File[]): Promise<Array<{ success: boolean; name: string; file: File }>> => { const results: Array<{ success: boolean; name: string; file: File }> = [] // 这里实际上你不需要真的一个个发请求,因为你的后端是一次性接收所有文件 // 但是为了容错,我们可以先全部放到 FormData 里,用 try-catch 包裹整个请求 // 如果请求失败,再降级为逐个上传 try { // 先尝试一次性上传所有文件 const formData = new FormData() files.forEach((file) => { formData.append('files', file) }) if (activeTab.value === 'employee') { const data = await GongShiAnalyzeApi(formData) getGongShiData(data) } else { const data = await QiHuaGongShiAnalyzeApi(formData) getGongShiData(data) } // 全部成功 return files.map(file => ({ success: true, name: file.name, file })) } catch (error) { // 如果一次性上传失败,可能是其中某个文件有问题,降级为逐个上传 console.warn('批量上传失败,降级为逐个上传:', error) ElMessage.info('批量上传失败,正在逐个重试...') for (const file of files) { try { const formData = new FormData() formData.append('files', file) if (activeTab.value === 'employee') { await GongShiAnalyzeApi(formData) } else { await QiHuaGongShiAnalyzeApi(formData) } results.push({ success: true, name: file.name, file }) } catch (uploadError) { console.error(`文件 ${file.name} 上传失败:`, uploadError) results.push({ success: false, name: file.name, file }) } } return results } }

关键改动说明

改动点原代码问题新方案
预检文件无预检,直接上传FileReader预读前4字节,检测文件是否可读
被占用文件处理整个请求直接失败弹窗告知用户哪些文件被占用,让用户选择"跳过"或"取消"
上传方式一次发送所有文件,一个失败全挂先尝试批量上传,失败后降级为逐个上传
错误提示只有console.log明确提示哪些文件失败,提供友好反馈
input 清空上传完成后清空target.value,便于再次选择相同文件夹

需要新增的引入

import { ElMessage, ElMessageBox } from 'element-plus'

如果不想弹窗,简化版本(仅跳过被占用文件)

如果你不想让用户选择,直接跳过被占用文件并提示,可以用这个简化版:

const triggerFolderUpload = () => { const input = folderUploadRef.value.$el.querySelector('input') as HTMLInputElement if (!input) return input.onchange = async (e: Event) => { const target = e.target as HTMLInputElement const files = Array.from(target.files || []) if (!files.length) return cur.value.tableLoading = true try { // 预检文件 const { readableFiles, unreadableFiles } = await preCheckFiles(files) if (unreadableFiles.length > 0) { ElMessage.warning(`已跳过 ${unreadableFiles.length} 个被占用的文件:${unreadableFiles.join('、')}`) } if (readableFiles.length === 0) { ElMessage.warning('没有可上传的文件') cur.value.tableLoading = false return } // 组装可读文件并上传 const formData = new FormData() readableFiles.forEach((file) => { formData.append('files', file) }) if (activeTab.value === 'employee') { const data = await GongShiAnalyzeApi(formData) getGongShiData(data) } else { const data = await QiHuaGongShiAnalyzeApi(formData) getGongShiData(data) } const firstPath = files[0]?.webkitRelativePath || '' cur.value.folderName = firstPath.includes('/') ? firstPath.split('/')[0] : '所选文件夹' cur.value.fileTotal = readableFiles.length ElMessage.success(`成功上传 ${readableFiles.length} 个文件${unreadableFiles.length > 0 ? `,跳过 ${unreadableFiles.length} 个被占用文件` : ''}`) } catch (error) { console.error('上传失败:', error) ElMessage.error('上传失败,请重试') cur.value.workHourList = [] cur.value.folderName = '' } finally { cur.value.tableLoading = false target.value = '' } } input.click() }

两种方案对比

弹窗版(完整)简化版(静默跳过)
用户体验明确告知,用户可自主决定自动跳过,但不告知具体哪些文件
代码复杂度稍复杂简单
适用场景正式生产环境内部工具或对容错要求不高的场景

建议使用弹窗版(完整版),用户能清晰知道发生了什么,避免上传后数据不完整却不知道原因。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/26 15:11:15

零依赖仅3KB的文本高亮魔法:Fokus JavaScript高亮库完整入门指南

零依赖仅3KB的文本高亮魔法&#xff1a;Fokus JavaScript高亮库完整入门指南 【免费下载链接】Fokus 项目地址: https://gitcode.com/gh_mirrors/fo/Fokus Fokus 是一款零依赖、体积仅约 3KB 的 JavaScript 文本高亮库&#xff1a;只要用户选中页面上的任意内容&#x…

作者头像 李华
网站建设 2026/8/26 15:11:02

从场景标题到对白:Fountain剧本格式全部12种元素类型速查清单

从场景标题到对白&#xff1a;Fountain剧本格式全部12种元素类型速查清单 【免费下载链接】Fountain An open source implementation of the Fountain screenplay formatting language. 项目地址: https://gitcode.com/gh_mirrors/foun/Fountain Fountain 是一种用纯文本…

作者头像 李华
网站建设 2026/8/26 15:11:01

基于SpringBoot的松居智能家居销售管理系统毕业设计项目源码文档

温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台…

作者头像 李华