ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

拆解 outsourceExcel.js:uni-app 多端选文件、路径归一与上传准备

拆解 outsourceExcel.js:uni-app 多端选文件、路径归一与上传准备 环境uni-app 3.x Vue3Vite 5HBuilderX 打包 App。覆盖 Android / iOS / H5 三条选文件路径。对象src/utils/outsourceExcel.js662 行导出了 10 个函数。场景外购单开单页点「导入」用户选一个.xls/.xlsx上传给后端解析成商品明细行工作台的「文件上传」走同一套选择器但不限类型——图片、PDF、Office 文档、zip 压缩包都能传。页面上只有一个「导入」按钮组件薄得几乎没有逻辑真正的活全在这个 662 行的工具文件里。它要解决三件事把文件从系统里选出来、把五花八门的路径压成一种形态、把文件送到uploadFile能认的位置。下面按文件的组织顺序逐段拆。一、文件全景10 个导出三类职责函数职责谁在用chooseOutsourceExcelFile选文件只认 Excel内部按平台分三套实现导入按钮prepareOutsourceExcelFile上传前准备App 端落沙盒选完文件后调chooseAnyFile选文件不限类型同一套实现换参数工作台「文件上传」prepareAnyFile任意文件的落沙盒保留原后缀工作台「文件上传」copyTempFileToNamedPath复制到_doc/并返回本地 URL被上面几个复用normalizeAppPickedFile路径归一把五种形态压成{path,name}内部未导出buildSupplierListFromQuotedtl后端返回的扁平行 → 页面要的分组结构导入成功后调getFileNameFromPath从路径里取文件名并解码上面几个共用getOutsourceFileExt/isOutsourceExcelName取扩展名 / 判断是不是 Excel导入按钮做前置校验getAnyFileExt取任意扩展名最多 10 位任意文件上传主线就一条选择 → 归一 → 交付。buildSupplierListFromQuotedtl属于「回来之后」的事放文末单说。二、入口一个函数三套实现对外只暴露一个chooseOutsourceExcelFile内部用条件编译分流exportconstchooseOutsourceExcelFile(){// #ifdef APP-PLUSreturnwithTimeout(chooseAppExcelFile(选择外购单模板,.xls,.xlsx,.xlsx),5*60*1000,选择文件超时请重试);// #endif// #ifdef H5returnnewPromise((resolve,reject){/* uni.chooseFile见第六节 */});// #endifreturnPromise.reject(newError(当前平台暂不支持导入 Excel 文件));};两个容易忽略的点条件编译块里必须return不能靠变量赋值否则打包后某个平台会落空末尾那句Promise.reject是兜底保证在没命中的平台上调用方拿到的是一个明确的 Error而不是undefined.then is not a function。注意这三个参数不是写死的第十一节就是靠传空串把它们关掉的。三、Android自己起一个 Intentuni.chooseFile的type: file在 App 端没有实现所以这一段只能写原生constmainplus.android.runtimeMainActivity();constIntentplus.android.importClass(android.content.Intent);constintentnewIntent(Intent.ACTION_GET_CONTENT);intent.setType(*/*);intent.addCategory(Intent.CATEGORY_OPENABLE);constPICK_CODE20307;constpreviousmain.onActivityResult;main.onActivityResultfunction(requestCode,resultCode,data){if(requestCode!PICK_CODE)returnprevious?.(requestCode,resultCode,data);if(resultCode!-1)returnreject(newError(cancel));// RESULT_OK 恒为 -1...};main.startActivityForResult(intent,PICK_CODE);plus.android.startActivityForResult文档里没有、回调也不触发唯一能拿到结果的办法就是重写Activity.onActivityResult。这段里的数字都是硬编码RESULT_OK -1、requestCode自选 20307改代码时别把它们当普通 magic number。四、content:// 的三级读取Android 10 之后分区存储生效_data列经常为空能拿到的只是content://而uploadFile只认文件路径。于是有了这个函数constresolvermain.getContentResolver();plus.android.importClass(resolver);constinputresolver.openInputStream(uri);constFileplus.android.importClass(java.io.File);constFileOutputStreamplus.android.importClass(java.io.FileOutputStream);constoutnewFileOutputStream(newFile(main.getCacheDir(),outsource_${Date.now()}.xlsx));letcopiedfalse;try{plus.android.importClass(android.os.FileUtils).copy(input,out);copiedtrue;}catch(e){}if(!copied){try{input.transferTo(out);copiedtrue;}catch(e){}}if(!copied){/* 8KB 字节流循环 write */}降级层级手段备注一层FileUtils.copy最快但部分 ROM 没有这个方法二层InputStream.transferToAPI 26低版本直接抛三层8KBbyte[]循环最慢但一定能跑函数末尾三件事不能省file.length() 0判空0 字节文件上传后后端只会说「模板无数据」排查方向全歪、finally关流、失败时返回空串而不是抛错——上层统一弹「无法读取所选文件」比把 Java 异常甩给用户好。五、iOS两个回调都可能是成功if(typeofplus.io.chooseFile!function){returnreject(newError(当前设备暂不支持系统文件选择));}plus.io.chooseFile({title:选择外购单模板,multiple:false,filter:.xls,.xlsx},(res){constppickPathFromPlusFileResult(res);p?resolve(p):reject(newError(未选择文件));},(err){if(/cancel|取消/i.test(String(err?.message||err?.errMsg||err)))returnreject(newError(cancel));constppickPathFromPlusFileResult(err);// 某些版本把结果塞在 fail 里p?resolve(p):reject(newError(err?.message||选择文件失败));});plus.io.chooseFile不在官方文档里所以先做存在性判断filter是逗号分隔的字符串不是数组部分系统版本会忽略fail回调里也要尝试解析一次结果这个写法看着别扭但在实际机型上确实遇到过「选完文件却走了 fail」。而且这个filter只是「建议」实测在 iOS 上照样能选到.docx、.pptx、.zip选完一样会进沙盒、一样能传。所以「能选」和「能传」是两件事客户端这层限制只能算引导——第十一节那个「什么都能传」的按钮就是靠传空filter做出来的。六、H5最短的一条分支uni.chooseFile返回blob:路径浏览器环境可以直接交给uploadFile所以 H5 分支不需要落沙盒代码只有十几行。唯一要记住的是blob:的生命周期只在当前页面刷新或跳转就失效想复用必须先上传换真实 URL。七、normalizeAppPickedFile把五种路径压成一种选完之后不直接上传先过一层归一。这一步是整个文件里分支最多的函数拿到的形态处理方式content://...先resolveAndroidContentPath再走内容流复制/storage/...绝对路径直接复制到沙盒file://...复制失败时原样返回能直接用路径没扩展名按.xlsx兜底复制失败copy → toLocalURL(5s) → file://拼接三级降级if(/^content:\/\//i.test(path)){pathresolveAndroidContentPath(path)||copyAndroidContentUriToCache(path,name);if(!path)thrownewError(无法读取所选文件请先将文件保存到手机存储后重试);}constextgetExt(name)||getExt(path)||defaultExt;// Excel 传 .xlsx任意文件传 constcopiedawaitcopyTempFileToNamedPath(path,outsource_${Date.now()}${ext});return{path:copied,name:name||外购单${ext}};注意两点入参和返回值里的path同名不同义入参可能是 URI返回值一定是沙盒里的file://地址getExt和defaultExt都是参数不是写死的常量所以同一个函数换个传参就成了「任意文件」版本。八、copyTempFileToNamedPath为什么要落_doc/plus.io.resolveLocalFileSystemURL(tempFilePath,(entry){plus.io.resolveLocalFileSystemURL(_doc/,(root){root.getDirectory(outsource_import/${Date.now()},{create:true},(dir){entry.copyTo(dir,fileName,(copied)resolve(copied.toLocalURL()),reject);},reject);},reject);},reject);三个原因content://不能直接上传、临时文件随时被系统清理、复制后uploadFile才认。放_doc/私有目录而非_downloads/是因为前者不需要存储权限目录按Date.now()分避免重名覆盖用toLocalURL()而不是toURL()后者在部分机型返回的是不可读的file:///storage/...。九、withTimeout三段超时不是随便定的constwithTimeout(promise,ms,message)newPromise((resolve,reject){consttimersetTimeout(()reject(newError(message)),ms);Promise.resolve(promise).then((v){clearTimeout(timer);resolve(v);},(e){clearTimeout(timer);reject(e);});});环节超时依据等用户选文件5 分钟人工操作找文件慢是常态prepare复制/归一15 秒本地 IO超过就是卡住了上传解析120 秒后端要读表、校验、回数据把选文件也设成 15 秒是最常见的错误——用户还在文件管理器里翻目录这边已经判超时了。十、三个小工具撑起了所有边界判断exportconstgetFileNameFromPath(path){constcleanString(path||).split(?)[0].split(#)[0];constnameclean.split(/[\\/]/).pop()||;try{returndecodeURIComponent(name);}catch(e){returnname;}};exportconstgetOutsourceFileExt(nameOrPath)(String(nameOrPath||).match(/\.(xls|xlsx)$/i)||[])[0].toLowerCase();exportconstisOutsourceExcelName(name)/\.(xls|xlsx)$/i.test(String(name||));decodeURIComponent必须包try/catch文件名里出现裸%会抛URIError。getOutsourceFileExt返回带点的扩展名拼文件名用isOutsourceExcelName只做布尔判断前置校验用两个别混用。但这层校验只能算「客气地劝一下」别当成安全边界它拦得住「有扩展名且不是 xls/xlsx」的文件却拦不住 iOS 上选出来的.zip没走这套判断或者压根没扩展名。真正的类型判断放在服务端——读文件头也好、按 MIME 也好至少不会把压缩包当表格解析。十一、同一个文件的另一半工作台的「文件上传」工作台那个蓝色云朵按钮没有自己的页面点下去走的是同一套选择器。它现在的定位很明确任意类型文件上传——图片jpg/png、PDF、Word/PPT、Excel、zip/rar 压缩包都走这一个入口前端不做任何扩展名过滤。// pages/workbench/index.vue{ignorePermission:true,type:fileUpload,text:文件上传,icon:cloud-upload}consthandleActionClick(item){if(item?.typefileUpload)returnuploadAnyFile();// 没有 path只能特判if(!item?.path)return;navigateTo(item.path);};constuploadAnyFileasync(){if(uploadingFile.value)return;// 防连点constfileawaitchooseAnyFile();// 选constpreparedawaitprepareAnyFile(file);// 落沙盒constdataawaituploadCommonFile(prepared.path);// POST /common/uploaduni.showToast({title:上传成功,icon:success});};和导入按钮相比区别只是三个参数参数Excel 导入任意文件上传filter.xls,.xlsx空串iOS 不设过滤器defaultExt.xlsx没后缀就不补getExtgetOutsourceFileExtgetAnyFileExt\.[a-z0-9]{1,10}$chooseExcelFileInIOS里写的是if (filter) options.filter filter传空串时系统选择器就不加任何类型限制——图片、.pdf、.docx、.pptx、.xlsx、.zip、.rar全都能选Android 那边本来就是setType(*/*)。同一份代码靠参数决定是「只收 Excel」还是「什么都收」这也是「filter 只能算引导」的由来。三个值得记一笔的细节沙盒目录仍叫outsource_import/时间戳/从导入功能继承来的看日志容易误解没有扩展名的文件会被复制成outsource_时间戳无后缀后端只能靠内容猜类型uploadCommonFile里formData: { id: 1 }是后端强制字段通用上传没有单据可关联这个 id 其实不承载语义。另外提醒一句这个入口是ignorePermission: true任何登录用户都能往服务器写文件上传成功只弹一句 toastaccessUrl拿到就丢了用户既不知道文件进了哪个目录也拿不到链接。要长期用至少得补「复制链接」和大小限制。十二、交棒准备完只做一件事返回{ path, name }之后文件的使命就结束剩下交给接口层uni.uploadFile打/saleOrder/importOutsourceExcelname: file、timeout: 120000、头里带 Bearer。响应兼容「信封」和「裸对象」两种结构判定顺序是 HTTP 2xx → 有无业务信封 → 载荷里有没有rowList失败记得reject(new Error(msg))传对象会让上层拿不到文案。十三、回来之后buildSupplierListFromQuotedtl后端返回的是扁平行页面要「供应商 → 明细行」两层结构。核心就一句分组键是spid 供应商名。constspKey(r)${r.spid??r.sp_id??}${String(r.spname??r.spName??).trim()};constkspKey(raw);if(!byKey.has(k))byKey.set(k,{cpny:raw.spname,id:raw.spid,cpal:raw.spcpal,fetchList:[]});三件事要在这个循环里补齐poamt为空时用采购价 × 数量算不然合计列是空的、img_ids转逗号串、actualSize/file_url统一成[{url}]——页面组件只认这两种形态。十四、复用这份文件的四条约定一个入口函数 条件编译平台差异全部封在文件里上层只认{ path, name }一切路径先归一凡是不以file://结尾的都当成不可信失败返回明确状态或Error不把原生异常抛给业务层超时按环节分级人工操作给足时间IO 和接口才收紧。按这四条写这套能力复用到采购单、调拨单只要换个接口地址和字段映射。最后补一句题外话选择器和扩展名校验都只是「引导用户选对」iOS 上 docx、ppt、zip 一样能选、能传。真要限制类型去服务端加判断别指望客户端这一层。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表