小程序双模式(文件 / 照片)上传组件封装与解析

0 阅读5分钟

在小程序业务开发中,上传功能是高频场景,而部分业务需要同时支持文件上传(如 PDF、文档)和照片上传(如图片凭证)两种模式。

基于完整的业务代码,从 WXML 结构、JS 逻辑、WXSS 样式三个维度,深度解析一个支持双模式切换、带上传 / 预览 / 删除、容错处理完善的小程序上传组件。该组件直接复用即可落地,适配绝大多数表单类业务(如用印申请、资料提交等)。

image.png

image.png

通过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;
}