当前位置: 首页 > news >正文

小程序文件上传怎么做?一套可复用的 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 class="page-wrap"> <view class="broker-page sign-enterprise-page"> <navbar title="上传与预览 Demo" :isBack="true" /> <card> <view class="demo-title">文件上传与预览(参考版)</view> <view class="demo-subtitle"> 用于后续业务接入:上传后展示状态,成功后支持真实文件预览 </view> <view class="demo-actions"> <button class="demo-btn demo-btn--primary" :disabled="uploading" @click="handleUpload"> {{ uploading ? '上传中...' : uploaded ? '重新上传文件' : '选择并上上传文件' }} </button> <button class="demo-btn demo-btn--ghost" :disabled="!uploaded" @click="handlePreview"> 预览已上传文件 </button> </view> <view class="demo-status" :class="statusClass"> <text>{{ statusText }}</text> </view> <view class="demo-file-info"> <view class="demo-file-row"> <text class="demo-file-label">文件名:</text> <text class="demo-file-value">{{ fileName || '-' }}</text> </view> <view class="demo-file-row"> <text class="demo-file-label">文件路径:</text> <text class="demo-file-value">{{ uploadedFilePath || '-' }}</text> </view> <view class="demo-file-row"> <text class="demo-file-label">允许格式:</text> <text class="demo-file-value">pdf / doc / docx</text> </view> </view> </card> </view> </view> </template> <script setup lang="ts"> import { computed, ref } from 'vue' /** * 1:上传状态机 * - idle: 未上传 * - uploading: 正在选择/上传 * - success: 上传成功 * - fail: 上传失败 */ type UploadState = 'idle' | 'uploading' | 'success' | 'fail' const uploadState = ref<UploadState>('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 lang="scss" 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>
http://www.jsqmd.com/news/578278/

相关文章:

  • python基于Hadoop的热点事件分析的设计与实现
  • JSTL 标签库 <c:forEach> 循环标签学习:数组+无序列表
  • 集成学习:为什么单打独斗不如“打群架”?(上篇)
  • 从‘失真’到‘保真’:一次搞懂手机和WIFI 6/7里DPD硬件的‘逆向思维’
  • Chrome扩展开发入门:手把手教你打造个性化New Tab页面
  • 打破设备壁垒:VR-Reversal实现3D内容自由视角全设备适配
  • 为什么 Ubuntu 24.04 不让你用 pip 了?从报错到 Python 环境管理的企业级方案
  • 跟着Cell学作图|10.蛋白质互作网络实战:GeNets数据库的机器学习驱动分析
  • 告别手动F4!SAP RAP开发中@Consumption.valueHelpDefinition的8个实战技巧与避坑指南
  • 小白小程序平台选型:5 大易上手平台深度对比 + 避坑指南 - 企业数字化改造和转型
  • 突破模型部署瓶颈:TimesFM 2.5从500M到200M的压缩实践指南
  • 呱呱赞、海橙子网、有赞、微盟、食亨:2026 外卖小程序哪家更靠谱? - 企业数字化改造和转型
  • 【声纳与人工智能融合——从理论前沿到自主系统实战(进阶篇)】第六章 旋转等变Transformer与声纳目标检测
  • LeetCode HOT100 - 寻找重复数
  • 5分钟搞定:Cesium/Leaflet/OpenLayers调用免费瓦片地图资源(附代码示例)
  • 国内主流CMS系统对比(2026年更新版)
  • 超自动化巡检:构筑业务连续性的第一道智能防线
  • 竞赛是否走的通
  • Spring AI 1.x 系列【22】深度拆解 ToolCallbackProvider 生命周期与调用链路
  • 2026年上海保洁服务推荐榜单:日常/精细/定点/厂房/开荒/装修后/别墅/展会/深度/商场保洁,专业高效的全场景洁净解决方案 - 品牌企业推荐师(官方)
  • 计算机毕业设计springboot在线运营工单处理系统 基于SpringBoot的客户服务工单流转与协同处理平台 SpringBoot框架下的智能运维服务请求跟踪管理系统
  • 2026年格兰富水泵厂家推荐排行榜:成套供水机组/无负压供水机组/供暖循环泵/空调循环泵/污水泵/污水提升泵/循环泵/不锈钢水泵/密封泵/螺杆泵,专业流体解决方案实力之选 - 品牌企业推荐师(官方)
  • 2026年AI风口已至!月薪3万+岗位盘点+零基础转行指南,速收藏!
  • 告别ArcGIS依赖!用QGIS 3.28把SHP属性表一键导出Excel,附赠3个数据清洗小技巧
  • 2026年 胶带厂家推荐排行榜:双面胶带/PET胶带/绝缘胶带/玛拉胶带/高温胶带/线圈胶带/保温胶带/透明胶带/警示胶带/布基胶带/美纹路胶带,精选粘接解决方案实力品牌! - 品牌企业推荐师(官方)
  • 3个AI视频总结功能让B站信息处理效率提升300%
  • 给我找一个能用的 typora 序列号 正版买了 爽 淘宝便宜 5 块
  • 3步搞定小红书无水印下载:XHS-Downloader开源神器实战全解析
  • 新闻科技简报 (2026-04-02)
  • 利用快马平台快速构建b站a8直播观看页面原型