尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

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

上传文件夹时有文件打开状态导致失败问题 背景使用ElementPlus的el-upload的directory上传文件夹属性时文件夹里的所有文件在没有打开的情况下是可以正常上传并且成功的但如果其中的某个文件处于打开状态上传就会出问题接口报红如图一所示Preview 如图二所示帮我看一下这是什么问题有什么好的解决思路和解决办法呢从你的描述和截图来看问题很明确当使用el-upload的directory属性上传文件夹时如果文件夹内有正在被其他程序打开占用的文件如正在被编辑的 Word/Excel、正在被预览的 PDF、正在被播放的视频等上传请求就会失败报net::ERR_FAILED。问题原因这不是 ElementPlus 或浏览器的问题而是操作系统文件系统的限制文件被独占锁定当某个程序以独占模式打开一个文件时例如 Microsoft Office 正在编辑 .docx 文件操作系统会阻止其他进程对该文件进行读取操作。浏览器无法读取input typefile webkitdirectory在获取文件列表时只能拿到文件元数据文件名、大小等但实际读取文件内容时浏览器会尝试通过系统 API 读取文件流。如果文件被锁定读取操作就会失败导致 XHR 请求直接报ERR_FAILED。前端无法感知浏览器 File API 在枚举文件时不会检查文件是否可读只有在真正开始读取上传时才会暴露问题。解决思路与方案方案一上传前预检 用户提示推荐在开始上传之前先尝试用FileReader或Blob.slice()预读每个文件的一小部分检测文件是否可读。如果不可读则提示用户关闭对应文件。// 预检函数 async function checkFileReadable(file: File): Promiseboolean { 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): Promiseboolean { 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): Promiseboolean { 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[]): PromiseArray{ 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() }两种方案对比弹窗版完整简化版静默跳过用户体验明确告知用户可自主决定自动跳过但不告知具体哪些文件代码复杂度稍复杂简单适用场景正式生产环境内部工具或对容错要求不高的场景建议使用弹窗版完整版用户能清晰知道发生了什么避免上传后数据不完整却不知道原因。
返回列表