在小程序业务开发中,上传功能是高频场景,而部分业务需要同时支持文件上传(如 PDF、文档)和照片上传(如图片凭证)两种模式。
基于完整的业务代码,从 WXML 结构、JS 逻辑、WXSS 样式三个维度,深度解析一个支持双模式切换、带上传 / 预览 / 删除、容错处理完善的小程序上传组件。该组件直接复用即可落地,适配绝大多数表单类业务(如用印申请、资料提交等)。
通过wx:if根据uploadMode切换文件 / 照片上传 UI,同时实现切换按钮、上传区域、删除 / 预览功能。
js模式切换、文件上传、照片上传、删除 / 预览、异常处理全流程逻辑,代码注释详细,直接复用。
<view class="seal_wrapper">
<view class="items_wrap">
<!-- 标题栏:带必填标识 -->
<view class="items_titles">
<text>*</text>用印文件原件
</view>
<!-- 模式切换开关:文件/照片 -->
<view class="upload-switch">
<view class="upload-switch-item {{uploadMode === 'file' ? 'active' : ''}}" bind:tap="setUploadMode" data-mode="file">文件</view>
<view class="upload-switch-item {{uploadMode === 'image' ? 'active' : ''}}" bind:tap="setUploadMode" data-mode="image">照片</view>
</view>
<!-- 上传内容区域:根据模式渲染 -->
<view class="items_input">
<!-- 文件上传模式 -->
<view class="file-uploader" wx:if="{{uploadMode === 'file'}}">
<!-- 已上传文件:显示文件名+删除按钮 -->
<view class="file-item" wx:if="{{fileUrl}}">
<view class="file-name">{{fileName || '已上传文件'}}</view>
<image class="file-del" src="/img/close.png" mode="aspectFit" bind:tap="removeFile" />
</view>
<!-- 未上传文件:显示上传入口 -->
<view class="file-add" bind:tap="chooseFile" wx:if="{{!fileUrl}}">
<image class="file-add-icon" src="/img/add.png" mode="aspectFit"/>
<view class="file-add-text">上传文件</view>
</view>
</view>
<!-- 照片上传模式 -->
<view class="images-grid" wx:if="{{uploadMode === 'image'}}">
<!-- 已上传照片:显示图片+删除+预览 -->
<view class="img-item" wx:if="{{images && images.length}}">
<image class="img" src="{{images[0]}}" mode="aspectFit" bind:tap="previewImage" data-idx="0"/>
<image class="img-del" src="/img/close.png" mode="aspectFit" bind:tap="removeImage" data-idx="0"/>
</view>
<!-- 未上传照片:显示上传入口 -->
<view class="img-add" bind:tap="chooseImages">
<image class="img-add-icon" src="/img/add.png" mode="aspectFit"/>
</view>
</view>
</view>
</view>
</view>
Page({
data: {
// 默认选中文件上传模式
uploadMode: 'file',
fileUrl: '', // 已上传文件的服务端链接
fileName: '', // 已上传文件名称
images: [] // 已上传照片链接数组
},
/**
* 切换上传模式(文件/照片)
*/
setUploadMode(e) {
const mode = e?.currentTarget?.dataset?.mode || ''
// 仅允许切换到合法模式
if (mode !== 'file' && mode !== 'image') return
this.setData({ uploadMode: mode })
},
/**
* 删除已上传文件
*/
removeFile() {
this.setData({
fileUrl: '',
fileName: ''
})
},
/**
* 选择并上传文件(文档)
*/
chooseFile() {
wx.chooseMessageFile({
count: 1, // 单次上传1个文件
type: 'file', // 类型为文件
success: (res) => {
const files = res?.tempFiles || []
const file = files[0] || null
if (!file?.path) return
// 保存文件名称
this.setData({ fileName: file.name || '' })
// 上传中loading
wx.showLoading({ title: '上传中...' })
// 调用上传接口
wx.uploadFile({
filePath: file.path,
name: 'file', // 服务端接收的文件字段名
url: api.ImgUpload(), // 替换为你的图片上传接口
formData: {
// 附加参数:用户身份标识
OpenID: wx.getStorageSync('openId'),
MID: wx.getStorageSync('mid'),
},
success: (resu) => {
// 解析服务端返回(捕获JSON解析异常)
let data = null
try {
data = JSON.parse(resu.data)
} catch (e) {
data = null
}
// 上传成功:保存文件链接
if (data && data.code === 200 && data.data) {
this.setData({ fileUrl: data.data })
return
}
// 上传失败:提示错误信息
wx.showToast({
title: data?.msg || '上传失败',
icon: 'none'
})
},
fail: (err) => {
console.error('文件上传失败:', err)
wx.showToast({ title: '上传失败', icon: 'none' })
},
complete: () => {
wx.hideLoading() // 无论成功失败,关闭loading
}
})
},
fail: (err) => {
console.error('选择文件失败:', err)
}
})
},
/**
* 预览照片
*/
previewImage(e) {
const idx = e?.currentTarget?.dataset?.idx || 0
const urls = this.data.images || []
if (!urls.length) return
// 调用小程序原生预览API
wx.previewImage({
urls,
current: urls[idx] || urls[0]
})
},
/**
* 删除已上传照片
*/
removeImage(e) {
const idx = e?.currentTarget?.dataset?.idx || 0
const urls = [...(this.data.images || [])] // 浅拷贝避免直接修改原数据
if (!urls.length) return
urls.splice(idx, 1) // 删除对应索引的图片
this.setData({ images: urls })
},
/**
* 选择并上传照片
*/
chooseImages() {
wx.chooseMedia({
count: 1, // 单次上传1张照片
mediaType: ['image'], // 仅选择图片
sourceType: ['album', 'camera'], // 支持相册/相机
success: (res) => {
const files = res?.tempFiles || []
const tempFilePath = files[0]?.tempFilePath || ''
if (!tempFilePath) return
wx.showLoading({ title: '上传中...' })
wx.uploadFile({
filePath: tempFilePath,
name: 'file',
url: api.ImgUpload(), // 替换为你的图片上传接口
formData: {
OpenID: wx.getStorageSync('openId'),
MID: wx.getStorageSync('mid'),
},
success: (resu) => {
let data = null
try {
data = JSON.parse(resu.data)
} catch (e) {
data = null
}
if (data && data.code === 200 && data.data) {
// 照片仅支持单张,直接覆盖数组
this.setData({ images: [data.data] })
return
}
wx.showToast({
title: data?.msg || '上传失败',
icon: 'none'
})
},
fail: (err) => {
console.error('照片上传失败:', err)
wx.showToast({ title: '上传失败', icon: 'none' })
},
complete: () => {
wx.hideLoading()
}
})
},
fail: (err) => {
console.error('选择照片失败:', err)
}
})
}
})
/* 外层容器:避免样式污染 */
.seal_wrapper .items_wrap {
width: 100%;
padding: 30rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
/* 最后一项去掉下边框 */
.seal_wrapper .items_wrap:last-child {
border-bottom: none;
}
/* 标题样式 */
.seal_wrapper .items_titles {
font-size: 32rpx;
margin-bottom: 20rpx;
color: #222;
}
/* 必填红色星号 */
.seal_wrapper .items_titles text {
color: #ff4d4f;
margin-right: 8rpx;
}
/* 输入/上传区域 */
.seal_wrapper .items_input {
width: 100%;
}
/* 模式切换容器 */
.seal_wrapper .upload-switch{
display: flex;
gap: 16rpx;
margin: 8rpx 0 16rpx;
}
/* 切换项样式 */
.seal_wrapper .upload-switch-item{
padding: 10rpx 26rpx;
border-radius: 999rpx;
background: #f3f4f6;
color: #666;
font-size: 26rpx;
line-height: 1.4;
}
/* 选中态样式 */
.seal_wrapper .upload-switch-item.active{
background: #e8f1ff;
color: #2f7cff;
font-weight: 700;
}
/* 文件上传容器 */
.seal_wrapper .file-uploader{
width: 100%;
}
/* 未上传文件:虚线边框+居中 */
.seal_wrapper .file-add{
width: 100%;
height: 96rpx;
border-radius: 16rpx;
border: 2rpx dashed #d8d8d8;
display: flex;
align-items: center;
justify-content: center;
gap: 14rpx;
}
/* 上传图标 */
.seal_wrapper .file-add-icon{
width: 44rpx;
height: 44rpx;
opacity: 0.7;
}
/* 上传文字 */
.seal_wrapper .file-add-text{
font-size: 28rpx;
color: #666;
}
/* 已上传文件:背景色+弹性布局 */
.seal_wrapper .file-item{
width: 100%;
min-height: 96rpx;
border-radius: 16rpx;
background: #f9fafb;
padding: 18rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
}
/* 文件名:超出省略 */
.seal_wrapper .file-name{
flex: 1;
min-width: 0;
font-size: 28rpx;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 16rpx;
}
/* 删除按钮 */
.seal_wrapper .file-del{
width: 36rpx;
height: 36rpx;
background: rgba(0,0,0,0.3);
border-radius: 18rpx;
padding: 4rpx;
box-sizing: border-box;
}
/* 照片网格布局 */
.seal_wrapper .images-grid{
display: flex;
flex-wrap: wrap;
gap: 18rpx;
}
/* 已上传照片容器 */
.seal_wrapper .img-item{
width: 160rpx;
height: 160rpx;
border-radius: 16rpx;
overflow: hidden;
position: relative;
}
/* 照片 */
.seal_wrapper .img{
width: 100%;
height: 100%;
}
/* 照片删除按钮 */
.seal_wrapper .img-del{
position: absolute;
top: 6rpx;
right: 6rpx;
width: 36rpx;
height: 36rpx;
background: rgba(0,0,0,0.3);
border-radius: 18rpx;
padding: 4rpx;
box-sizing: border-box;
}
/* 未上传照片:虚线边框+居中 */
.seal_wrapper .img-add{
width: 160rpx;
height: 160rpx;
border-radius: 16rpx;
border: 2rpx dashed #d8d8d8;
display: flex;
align-items: center;
justify-content: center;
}
/* 上传图标 */
.seal_wrapper .img-add-icon{
width: 54rpx;
height: 54rpx;
opacity: 0.7;
}