小程序文件上传怎么做?一套可复用的 UniApp 上传+预览 Demo

张开发
2026/4/6 17:39:17 15 分钟阅读

分享文章

小程序文件上传怎么做?一套可复用的 UniApp 上传+预览 Demo
在 UniApp 里做文件上传并不难难的是把“用户体验”做完整上传是否成功、失败了为什么、上传后能不能预览刚才那份文件。本文用一个企业签约页面 demo完整实现pdf/doc/docx上传、状态机提示与openDocument真实预览并补齐多端返回结构差异和失败兜底逻辑代码可以直接复用到业务项目里。UniApp 上传文件最佳实践上传结果提示、失败兜底与文档预览上传文件后要“看得见状态、点得开文件”最终效果支持格式pdf / doc / docx上传过程有状态未上传、上传中、上传成功、上传失败上传成功后点击“预览”可打开刚上传文件不同端返回结构不一致时也能兼容状态模型设计核心推荐状态机idle - uploading - success/fail为什么不用多个布尔值硬拼可读性差、容易出现矛盾状态实际落地uploadState业务状态uploading按钮禁用控制uploadedFilePath预览依赖字段上传实现细节handleUpload上传前提示uni.showToast 延迟 1500ms 再打开选择器选择文件chooseMessageFile微信端能力关键兼容优先从tempFiles[0]取name/path/tempFilePath兜底用tempFilePaths[0]成功策略拿到路径才算可预览成功只拿到文件名但无路径时给明确提示预览实现细节handlePreview入口判断未上传或无路径直接提示真实预览uni.openDocument({ filePath, showMenu: true })失败兜底预览失败时更新状态并 toast 告知总结上传功能的关键不只是“能传”而是“状态清晰 可预览 有兜底”这套 demo 可直接迁移到签约、资料提交、附件审核等场景template view classpage-wrap view classbroker-page sign-enterprise-page navbar title上传与预览 Demo :isBacktrue / card view classdemo-title文件上传与预览参考版/view view classdemo-subtitle 用于后续业务接入上传后展示状态成功后支持真实文件预览 /view view classdemo-actions button classdemo-btn demo-btn--primary :disableduploading clickhandleUpload {{ uploading ? 上传中... : uploaded ? 重新上传文件 : 选择并上上传文件 }} /button button classdemo-btn demo-btn--ghost :disabled!uploaded clickhandlePreview 预览已上传文件 /button /view view classdemo-status :classstatusClass text{{ statusText }}/text /view view classdemo-file-info view classdemo-file-row text classdemo-file-label文件名/text text classdemo-file-value{{ fileName || - }}/text /view view classdemo-file-row text classdemo-file-label文件路径/text text classdemo-file-value{{ uploadedFilePath || - }}/text /view view classdemo-file-row text classdemo-file-label允许格式/text text classdemo-file-valuepdf / doc / docx/text /view /view /card /view /view /template script setup langts import { computed, ref } from vue /** * 1上传状态机 * - idle: 未上传 * - uploading: 正在选择/上传 * - success: 上传成功 * - fail: 上传失败 */ type UploadState idle | uploading | success | fail const uploadState refUploadState(idle) const uploaded ref(false) const uploading ref(false) const fileName ref() const uploadedFilePath ref() const waitTime 1500 async function handleUpload() { if (uploading.value) return uploading.value true uploadState.value uploading try { const chooseMessageFile (uni as any).chooseMessageFile if (typeof chooseMessageFile ! function) { uploadState.value fail uploaded.value false uni.showToast({ title: 当前环境不支持 chooseMessageFile, icon: none }) return } uni.showToast({ title: 当前仅可上传pdf、doc、docx格式的文档, icon: none, duration: waitTime, }) await new Promise((resolve) setTimeout(resolve, waitTime)) const res: any await new Promise((resolve, reject) { chooseMessageFile({ type: file, count: 1, extension: [pdf, doc, docx], success: resolve, fail: reject, }) }) /** * 2兼容不同返回结构 * 部分端返回 tempFiles部分端可能只有 tempFilePaths */ const file res?.tempFiles?.[0] || {} const rawPath file?.path || file?.tempFilePath || res?.tempFilePaths?.[0] || uploaded.value true fileName.value file?.name || file?.fileName || 已上传文件 uploadedFilePath.value typeof rawPath string ? rawPath : uploadState.value uploadedFilePath.value ? success : fail if (uploadState.value success) { uni.showToast({ title: 上传成功, icon: success }) } else { uni.showToast({ title: 上传成功但未拿到文件路径, icon: none }) } } catch (e) { uploadState.value fail uploaded.value false fileName.value uploadedFilePath.value uni.showToast({ title: 上传失败请重试, icon: none }) } finally { uploading.value false } } /** * 3成功后预览真实文件 * 使用 uni.openDocument 打开刚上传的本地临时文件 */ function handlePreview() { if (!uploaded.value || !uploadedFilePath.value) { uni.showToast({ title: 暂无可预览文件请先上传, icon: none }) return } uni.openDocument({ filePath: uploadedFilePath.value, showMenu: true, success: () { uni.showToast({ title: 已打开文件, icon: none }) }, fail: () { uploadState.value fail uni.showToast({ title: 文件预览失败, icon: none }) }, }) } const statusText computed(() { if (uploadState.value uploading) return 状态上传中 if (uploadState.value success) return 状态上传成功可预览 if (uploadState.value fail) return 状态上传/预览失败请重试 return 状态未上传 }) const statusClass computed(() { if (uploadState.value success) return is-success if (uploadState.value fail) return is-fail if (uploadState.value uploading) return is-uploading return is-idle }) /script style langscss scoped import ../style/detailIndex; .sign-enterprise-page { padding-bottom: 40rpx; } .demo-title { font-size: 30rpx; font-weight: 700; color: #111827; } .demo-subtitle { margin-top: 12rpx; color: #6b7280; font-size: 24rpx; line-height: 1.6; } .demo-actions { margin-top: 24rpx; display: flex; flex-direction: column; gap: 16rpx; } .demo-btn { border-radius: 12rpx; font-size: 28rpx; } .demo-btn--primary { background: #10b981; color: #ffffff; } .demo-btn--ghost { background: #ffffff; color: #065f46; border: 1px solid #10b981; } .demo-status { margin-top: 20rpx; padding: 14rpx 16rpx; border-radius: 10rpx; font-size: 24rpx; } .demo-status.is-idle { background: #f3f4f6; color: #4b5563; } .demo-status.is-uploading { background: #eff6ff; color: #1d4ed8; } .demo-status.is-success { background: #ecfdf5; color: #047857; } .demo-status.is-fail { background: #fef2f2; color: #b91c1c; } .demo-file-info { margin-top: 18rpx; display: flex; flex-direction: column; gap: 10rpx; } .demo-file-row { display: flex; align-items: baseline; gap: 8rpx; } .demo-file-label { font-size: 24rpx; color: #6b7280; } .demo-file-value { font-size: 24rpx; color: #111827; word-break: break-all; } /style

更多文章