<template> 
 | 
  <view v-if="!disabled" class="tn-image-upload-class tn-image-upload"> 
 | 
    <block v-if="showUploadList"> 
 | 
      <view 
 | 
        v-for="(item, index) in lists" 
 | 
        :key="index" 
 | 
        class="tn-image-upload__item tn-image-upload__item-preview" 
 | 
        :style="{ 
 | 
          width: $t.string.getLengthUnitValue(width), 
 | 
          height: $t.string.getLengthUnitValue(height) 
 | 
        }" 
 | 
      > 
 | 
        <!-- 删除按钮 --> 
 | 
        <view 
 | 
          v-if="deleteable" 
 | 
          class="tn-image-upload__item-preview__delete" 
 | 
          @tap.stop="deleteItem(index)" 
 | 
          :style="{ 
 | 
            borderTopColor: deleteBackgroundColor 
 | 
          }" 
 | 
        > 
 | 
          <view 
 | 
            class="tn-image-upload__item-preview__delete--icon" 
 | 
            :class="[`tn-icon-${deleteIcon}`]" 
 | 
            :style="{ 
 | 
              color: deleteColor 
 | 
            }" 
 | 
          ></view> 
 | 
        </view> 
 | 
        <!-- 进度条 --> 
 | 
        <tn-line-progress 
 | 
          v-if="showProgress && item.progress > 0 && !item.error" 
 | 
          class="tn-image-upload__item-preview__progress" 
 | 
          :percent="item.progress" 
 | 
          :showPercent="false" 
 | 
          :round="false" 
 | 
          :height="8" 
 | 
        ></tn-line-progress> 
 | 
        <!-- 重试按钮 --> 
 | 
        <view v-if="item.error" class="tn-image-upload__item-preview__error-btn" @tap.stop="retry(index)">点击重试</view> 
 | 
        <!-- 图片信息 --> 
 | 
        <image 
 | 
          class="tn-image-upload__item-preview__image" 
 | 
          :src="item.url || item.path" 
 | 
          :mode="imageMode" 
 | 
          @tap.stop="doPreviewImage(item.url || item.path, index)" 
 | 
        ></image> 
 | 
      </view> 
 | 
    </block> 
 | 
    <!-- <view v-if="$slots.file || $slots.$file" style="width: 100%;"> 
 | 
       
 | 
    </view> --> 
 | 
    <!-- 自定义图片展示列表 --> 
 | 
    <slot name="file" :file="lists"></slot> 
 | 
     
 | 
    <!-- 添加按钮 --> 
 | 
    <view v-if="maxCount > lists.length" class="tn-image-upload__add" :class="{'tn-image-upload__add--custom': customBtn}" @tap="selectFile"> 
 | 
      <!-- 添加按钮 --> 
 | 
      <view 
 | 
        v-if="!customBtn" 
 | 
        class="tn-image-upload__item tn-image-upload__item-add" 
 | 
        hover-class="tn-hover-class" 
 | 
        hover-stay-time="150" 
 | 
        :style="{ 
 | 
          width: $t.string.getLengthUnitValue(width), 
 | 
          height: $t.string.getLengthUnitValue(height) 
 | 
        }" 
 | 
      > 
 | 
        <view class="tn-image-upload__item-add--icon tn-icon-add"></view> 
 | 
        <view class="tn-image-upload__item-add__tips">{{ uploadText }}</view> 
 | 
      </view> 
 | 
      <!-- 自定义添加按钮 --> 
 | 
      <view> 
 | 
        <slot name="addBtn"></slot> 
 | 
      </view> 
 | 
    </view> 
 | 
  </view> 
 | 
</template> 
 | 
  
 | 
<script> 
 | 
  export default { 
 | 
    name: 'tn-image-upload', 
 | 
    props: { 
 | 
      // 已上传的文件列表 
 | 
      fileList: { 
 | 
        type: Array, 
 | 
        default() { 
 | 
          return [] 
 | 
        } 
 | 
      }, 
 | 
      // 上传图片地址 
 | 
      action: { 
 | 
        type: String, 
 | 
        default: '' 
 | 
      }, 
 | 
      // 上传文件的字段名称 
 | 
      name: { 
 | 
        type: String, 
 | 
        default: 'file' 
 | 
      }, 
 | 
      // 头部信息 
 | 
      header: { 
 | 
        type: Object, 
 | 
        default() { 
 | 
          return {} 
 | 
        } 
 | 
      }, 
 | 
      // 携带的参数 
 | 
      formData: { 
 | 
        type: Object, 
 | 
        default() { 
 | 
          return {} 
 | 
        } 
 | 
      }, 
 | 
      // 是否禁用 
 | 
      disabled: { 
 | 
        type: Boolean, 
 | 
        default: false 
 | 
      }, 
 | 
      // 是否自动上传 
 | 
      autoUpload: { 
 | 
        type: Boolean, 
 | 
        default: true 
 | 
      }, 
 | 
      // 最大上传数量 
 | 
      maxCount: { 
 | 
        type: Number, 
 | 
        default: 9 
 | 
      }, 
 | 
      // 是否显示组件自带的图片预览 
 | 
      showUploadList: { 
 | 
        type: Boolean, 
 | 
        default: true 
 | 
      }, 
 | 
      // 预览上传图片的裁剪模式 
 | 
      imageMode: { 
 | 
        type: String, 
 | 
        default: 'aspectFill' 
 | 
      }, 
 | 
      // 点击图片是否全屏预览 
 | 
      previewFullImage: { 
 | 
        type: Boolean, 
 | 
        default: true 
 | 
      }, 
 | 
      // 是否显示进度条 
 | 
      showProgress: { 
 | 
        type: Boolean, 
 | 
        default: true 
 | 
      }, 
 | 
      // 是否显示删除按钮 
 | 
      deleteable: { 
 | 
        type: Boolean, 
 | 
        default: true 
 | 
      }, 
 | 
      // 删除按钮图标 
 | 
      deleteIcon: { 
 | 
        type: String, 
 | 
        default: 'close' 
 | 
      }, 
 | 
      // 删除按钮的背景颜色 
 | 
      deleteBackgroundColor: { 
 | 
        type: String, 
 | 
        default: '' 
 | 
      }, 
 | 
      // 删除按钮的颜色 
 | 
      deleteColor: { 
 | 
        type: String, 
 | 
        default: '' 
 | 
      }, 
 | 
      // 上传区域提示文字 
 | 
      uploadText: { 
 | 
        type: String, 
 | 
        default: '选择图片' 
 | 
      }, 
 | 
      // 显示toast提示 
 | 
      showTips: { 
 | 
        type: Boolean, 
 | 
        default: true 
 | 
      }, 
 | 
      // 自定义选择图标按钮 
 | 
      customBtn: { 
 | 
        type: Boolean, 
 | 
        default: false 
 | 
      }, 
 | 
      // 预览图片和选择图片区域的宽度 
 | 
      width: { 
 | 
        type: Number, 
 | 
        default: 200 
 | 
      }, 
 | 
      // 预览图片和选择图片区域的高度 
 | 
      height: { 
 | 
        type: Number, 
 | 
        default: 200 
 | 
      }, 
 | 
      // 选择图片的尺寸 
 | 
      // 参考上传文档 https://uniapp.dcloud.io/api/media/image 
 | 
      sizeType: { 
 | 
        type: Array, 
 | 
        default() { 
 | 
          return ['original', 'compressed'] 
 | 
        } 
 | 
      }, 
 | 
      // 图片来源 
 | 
      sourceType: { 
 | 
        type: Array, 
 | 
        default() { 
 | 
          return ['album', 'camera'] 
 | 
        } 
 | 
      }, 
 | 
      // 是否可以多选 
 | 
      multiple: { 
 | 
        type: Boolean, 
 | 
        default: true 
 | 
      }, 
 | 
      // 文件大小(byte) 
 | 
      maxSize: { 
 | 
        type: Number, 
 | 
        default: 10 * 1024 * 1024 
 | 
      }, 
 | 
      // 允许上传的类型 
 | 
      limitType: { 
 | 
        type: Array, 
 | 
        default() { 
 | 
          return ['png','jpg','jpeg','webp','gif','image'] 
 | 
        } 
 | 
      }, 
 | 
      // 是否自定转换为json 
 | 
      toJson: { 
 | 
        type: Boolean, 
 | 
        default: true 
 | 
      }, 
 | 
      // 上传前钩子函数,每个文件上传前都会执行 
 | 
      beforeUpload: { 
 | 
        type: Function, 
 | 
        default: null 
 | 
      }, 
 | 
      // 删除文件前钩子函数 
 | 
      beforeRemove: { 
 | 
        type: Function, 
 | 
        default: null 
 | 
      }, 
 | 
      index: { 
 | 
        type: [Number, String], 
 | 
        default: '' 
 | 
      } 
 | 
    }, 
 | 
    computed: { 
 | 
       
 | 
    }, 
 | 
    data() { 
 | 
      return { 
 | 
        lists: [], 
 | 
        uploading: false 
 | 
      } 
 | 
    }, 
 | 
    watch: { 
 | 
      fileList: { 
 | 
        handler(val) { 
 | 
          val.map(value => { 
 | 
            // 首先检查内部是否已经添加过这张图片,因为外部绑定了一个对象给fileList的话(对象引用),进行修改外部fileList时, 
 | 
            // 会触发watch,导致重新把原来的图片再次添加到this.lists 
 | 
            // 数组的some方法意思是,只要数组元素有任意一个元素条件符合,就返回true,而另一个数组的every方法的意思是数组所有元素都符合条件才返回true 
 | 
            let tmp = this.lists.some(listVal => { 
 | 
              return listVal.url === value.url 
 | 
            }) 
 | 
            // 如果内部没有这张图片,则添加到内部 
 | 
            !tmp && this.lists.push({ url: value.url, error: false, progress: 100 }) 
 | 
          }) 
 | 
        }, 
 | 
        immediate: true 
 | 
      }, 
 | 
      lists(val) { 
 | 
        this.$emit('on-list-change', val, this.index) 
 | 
      } 
 | 
    }, 
 | 
    methods: { 
 | 
      // 清除列表 
 | 
      clear() { 
 | 
        this.lists = [] 
 | 
      }, 
 | 
      // 重新上传队列中上传失败所有文件 
 | 
      reUpload() { 
 | 
        this.uploadFile() 
 | 
      }, 
 | 
      // 选择图片 
 | 
      selectFile() { 
 | 
        if (this.disabled) return 
 | 
        const { 
 | 
          name = '', 
 | 
          maxCount, 
 | 
          multiple, 
 | 
          maxSize, 
 | 
          sizeType, 
 | 
          lists, 
 | 
          camera, 
 | 
          compressed, 
 | 
          sourceType 
 | 
        } = this 
 | 
        let chooseFile = null 
 | 
        const newMaxCount = maxCount - lists.length 
 | 
        // 只选择图片的时候使用 chooseImage 来实现 
 | 
        chooseFile = new Promise((resolve, reject) => { 
 | 
          uni.chooseImage({ 
 | 
            count: multiple ? (newMaxCount > 9 ? 9 : newMaxCount) : 1, 
 | 
            sourceType, 
 | 
            sizeType, 
 | 
            success: resolve, 
 | 
            fail: reject 
 | 
          }) 
 | 
        }) 
 | 
        chooseFile.then(res => { 
 | 
          let file = null 
 | 
          let listOldLength = lists.length 
 | 
          res.tempFiles.map((val, index) => { 
 | 
            if (!this.checkFileExt(val)) return 
 | 
             
 | 
            // 是否超出最大限制数量 
 | 
            if (!multiple && index >= 1) return 
 | 
            if (val.size > maxSize) { 
 | 
              this.$emit('on-oversize', val, lists, this.index) 
 | 
              this.showToast('超出可允许文件大小') 
 | 
            } else { 
 | 
              if (maxCount <= lists.length) { 
 | 
                this.$emit('on-exceed', val, lists, this.index) 
 | 
                this.showToast('超出最大允许的文件数') 
 | 
                return 
 | 
              } 
 | 
              lists.push({ 
 | 
                url: val.path, 
 | 
                progress: 0, 
 | 
                error: false, 
 | 
                file: val 
 | 
              }) 
 | 
            } 
 | 
          }) 
 | 
          this.$emit('on-choose-complete', this.lists, this.index) 
 | 
          if (this.autoUpload) this.uploadFile(listOldLength) 
 | 
        }).catch(err => { 
 | 
          this.$emit('on-choose-fail', err) 
 | 
        }) 
 | 
      }, 
 | 
      // 提示用户信息 
 | 
      showToast(message, force = false) { 
 | 
        if (this.showTips || force) { 
 | 
          this.$t.message.toast(message) 
 | 
        } 
 | 
      }, 
 | 
      // 手动上传,通过ref进行调用 
 | 
      upload() { 
 | 
        this.uploadFile() 
 | 
      }, 
 | 
      // 对失败图片进行再次上传 
 | 
      retry(index) { 
 | 
        this.lists[index].progress = 0 
 | 
        this.lists[index].error = false 
 | 
        this.lists[index].response = null 
 | 
        this.$t.message.loading('重新上传') 
 | 
        this.uploadFile(index) 
 | 
      }, 
 | 
      // 上传文件 
 | 
      async uploadFile(index = 0) { 
 | 
        if (this.disabled) return 
 | 
        if (this.uploading) return 
 | 
        // 全部上传完成 
 | 
        if (index >= this.lists.length) { 
 | 
          this.$emit('on-uploaded', this.lists, this.index) 
 | 
          return 
 | 
        } 
 | 
        // 检查是否已经全部上传或者上传中 
 | 
        if (this.lists[index].progress === 100) { 
 | 
          this.lists[index].uploadTask = null 
 | 
          if (this.autoUpload) this.uploadFile(index + 1) 
 | 
          return 
 | 
        } 
 | 
        // 执行before-upload钩子 
 | 
        if (this.beforeUpload && typeof(this.beforeUpload) === 'function') { 
 | 
          // 在微信,支付宝等环境(H5正常),会导致父组件定义的函数体中的this变成子组件的this 
 | 
          // 通过bind()方法,绑定父组件的this,让this的this为父组件的上下文 
 | 
          // 因为upload组件可能会被嵌套在其他组件内,比如tn-form,这时this.$parent其实为tn-form的this, 
 | 
          // 非页面的this,所以这里需要往上历遍,一直寻找到最顶端的$parent,这里用了this.$u.$parent.call(this) 
 | 
          let beforeResponse = this.beforeUpload.bind(this.$t.$parent.call(this))(index, this.lists) 
 | 
          // 判断是否返回了Promise 
 | 
          if (!!beforeResponse && typeof beforeResponse.then === 'function') { 
 | 
            await beforeResponse.then(res => { 
 | 
              // promise返回成功,不进行操作继续 
 | 
            }).catch(err => { 
 | 
              // 进入catch回调的话,继续下一张 
 | 
              return this.uploadFile(index + 1) 
 | 
            }) 
 | 
          } else if (beforeResponse === false) { 
 | 
            // 如果返回flase,继续下一张图片上传 
 | 
            return this.uploadFile(index + 1) 
 | 
          } else { 
 | 
            // 为true的情况,不进行操作 
 | 
          } 
 | 
        } 
 | 
        // 检查上传地址 
 | 
        if (!this.action) { 
 | 
          this.showToast('请配置上传地址', true) 
 | 
          return 
 | 
        } 
 | 
        this.lists[index].error = false 
 | 
        this.uploading = true 
 | 
        // 创建上传对象 
 | 
        const task = uni.uploadFile({ 
 | 
          url: this.action, 
 | 
          filePath: this.lists[index].url, 
 | 
          name: this.name, 
 | 
          formData: this.formData, 
 | 
          header: this.header, 
 | 
          success: res => { 
 | 
            // 判断啊是否为json字符串,将其转换为json格式 
 | 
            let data = this.toJson && this.$t.test.jsonString(res.data) ? JSON.parse(res.data) : res.data 
 | 
            if (![200, 201, 204].includes(res.statusCode)) { 
 | 
              this.uploadError(index, data) 
 | 
            } else { 
 | 
              this.lists[index].response = data 
 | 
              this.lists[index].progress = 100 
 | 
              this.lists[index].error = false 
 | 
              this.$emit('on-success', data, index, this.lists, this.index) 
 | 
            } 
 | 
          }, 
 | 
          fail: err => { 
 | 
            this.uploadError(index, err) 
 | 
          }, 
 | 
          complete: res => { 
 | 
            this.$t.message.closeLoading() 
 | 
            this.uploading = false 
 | 
            this.uploadFile(index + 1) 
 | 
            this.$emit('on-change', res, index, this.lists, this.index) 
 | 
          } 
 | 
        }) 
 | 
        this.lists[index].uploadTask = task 
 | 
        task.onProgressUpdate(res => { 
 | 
          if (res.progress > 0) { 
 | 
            this.lists[index].progress = res.progress 
 | 
            this.$emit('on-progress', res, index, this.lists, this.index) 
 | 
          } 
 | 
        }) 
 | 
      }, 
 | 
      // 上传失败 
 | 
      uploadError(index, err) { 
 | 
        this.lists[index].progress = 0 
 | 
        this.lists[index].error = true 
 | 
        this.lists[index].response = null 
 | 
        this.showToast('上传失败,请重试') 
 | 
        this.$emit('on-error', err, index, this.lists, this.index) 
 | 
      }, 
 | 
      // 删除一个图片 
 | 
      deleteItem(index) { 
 | 
        if (!this.deleteable) return 
 | 
        this.$t.message.modal( 
 | 
          '提示', 
 | 
          '您确定要删除吗?', 
 | 
          async () => { 
 | 
            // 先检查是否有定义before-remove移除前钩子 
 | 
            // 执行before-remove钩子 
 | 
            if (this.beforeRemove && typeof(this.beforeRemove) === 'function') { 
 | 
              let beforeResponse = this.beforeRemove.bind(this.$t.$parent.call(this))(index, this.lists) 
 | 
              // 判断是否返回promise  
 | 
              if (!!beforeResponse && typeof beforeResponse.then === 'function') { 
 | 
                await beforeResponse.then(res => { 
 | 
                  // promise返回成功不进行操作 
 | 
                  this.handlerDeleteItem(index) 
 | 
                }).catch(err => { 
 | 
                  this.showToast('删除操作被中断') 
 | 
                }) 
 | 
              } else if (beforeResponse === false) { 
 | 
                this.showToast('删除操作被中断') 
 | 
              } else { 
 | 
                this.handlerDeleteItem(index) 
 | 
              } 
 | 
            } else { 
 | 
              this.handlerDeleteItem(index) 
 | 
            } 
 | 
          }, true) 
 | 
      }, 
 | 
      // 移除文件操作 
 | 
      handlerDeleteItem(index) { 
 | 
        // 如果文件正在上传中,终止上传任务 
 | 
        if (this.lists[index].progress < 100 && this.lists[index].progress > 0) { 
 | 
          typeof this.lists[index].uploadTask !== 'undefined' && this.lists[index].uploadTask.abort() 
 | 
        } 
 | 
        this.lists.splice(index, 1) 
 | 
        this.$forceUpdate() 
 | 
        this.$emit('on-remove', index, this.lists, this.index) 
 | 
        this.showToast('删除成功') 
 | 
      }, 
 | 
      // 移除文件,通过ref手动形式进行调用 
 | 
      remove(index) { 
 | 
        if (!this.deleteable) return 
 | 
        // 判断索引合法 
 | 
        if (index >= 0 && index < this.lists.length) { 
 | 
          this.lists.splice(index, 1) 
 | 
        } 
 | 
      }, 
 | 
      // 预览图片 
 | 
      doPreviewImage(url, index) { 
 | 
        if (!this.previewFullImage) return 
 | 
        const images = this.lists.map(item => item.url || item.path) 
 | 
        uni.previewImage({ 
 | 
          urls: images, 
 | 
          current: url, 
 | 
          success: () => { 
 | 
            this.$emit('on-preview', url, this.lists, this.index) 
 | 
          }, 
 | 
          fail: () => { 
 | 
            this.showToast('预览图片失败') 
 | 
          } 
 | 
        }) 
 | 
      }, 
 | 
      // 检查文件后缀是否合法 
 | 
      checkFileExt(file) { 
 | 
        // 是否为合法后缀 
 | 
        let noArrowExt = false 
 | 
        // 后缀名 
 | 
        let fileExt = '' 
 | 
        const reg = /.+\./ 
 | 
         
 | 
        // #ifdef H5 
 | 
        fileExt = file.name.replace(reg, '').toLowerCase() 
 | 
        // #endif 
 | 
        // #ifndef H5 
 | 
        fileExt = file.path.replace(reg, '').toLowerCase() 
 | 
        // #endif 
 | 
        noArrowExt = this.limitType.some(ext => { 
 | 
          return ext.toLowerCase() === fileExt 
 | 
        }) 
 | 
        if (!noArrowExt) this.showToast(`不支持${fileExt}格式的文件`) 
 | 
        return noArrowExt 
 | 
      } 
 | 
    } 
 | 
  } 
 | 
</script> 
 | 
  
 | 
<style lang="scss" scoped> 
 | 
   
 | 
  .tn-image-upload { 
 | 
    display: flex; 
 | 
    flex-direction: row; 
 | 
    flex-wrap: wrap; 
 | 
    align-items: center; 
 | 
     
 | 
    &__item { 
 | 
      /* #ifndef APP-NVUE */ 
 | 
      display: flex; 
 | 
      /* #endif */ 
 | 
      align-items: center; 
 | 
      justify-content: center; 
 | 
      width: 200rpx; 
 | 
      height: 200rpx; 
 | 
      overflow: hidden; 
 | 
      margin: 12rpx; 
 | 
      margin-left: 0; 
 | 
      background-color: $tn-font-holder-color; 
 | 
      position: relative; 
 | 
      border-radius: 10rpx; 
 | 
       
 | 
      &-preview { 
 | 
        border: 1rpx solid $tn-border-solid-color; 
 | 
         
 | 
        &__delete { 
 | 
          display: flex; 
 | 
          align-items: center; 
 | 
          justify-content: center; 
 | 
          position: absolute; 
 | 
          top: 0; 
 | 
          right: 0; 
 | 
          z-index: 10; 
 | 
          border-top: 60rpx solid; 
 | 
          border-left: 60rpx solid transparent; 
 | 
          border-top-color: $tn-color-red; 
 | 
          width: 0rpx; 
 | 
          height: 0rpx; 
 | 
           
 | 
          &--icon { 
 | 
            position: absolute; 
 | 
            top: -50rpx; 
 | 
            right: 6rpx; 
 | 
            color: #FFFFFF; 
 | 
            font-size: 24rpx; 
 | 
            line-height: 1; 
 | 
          } 
 | 
        } 
 | 
         
 | 
        &__progress { 
 | 
          position: absolute; 
 | 
          width: auto; 
 | 
          bottom: 0rpx; 
 | 
          left: 0rpx; 
 | 
          right: 0rpx; 
 | 
          z-index: 9; 
 | 
          /* #ifdef MP-WEIXIN */ 
 | 
          display: inline-flex; 
 | 
          /* #endif */ 
 | 
        } 
 | 
         
 | 
        &__error-btn { 
 | 
          position: absolute; 
 | 
          bottom: 0; 
 | 
          left: 0; 
 | 
          right: 0; 
 | 
          background-color: $tn-color-red; 
 | 
          color: #FFFFFF; 
 | 
          font-size: 20rpx; 
 | 
          padding: 8rpx 0; 
 | 
          text-align: center; 
 | 
          z-index: 9; 
 | 
          line-height: 1; 
 | 
        } 
 | 
         
 | 
        &__image { 
 | 
          display: block; 
 | 
          width: 100%; 
 | 
          height: 100%; 
 | 
          border-radius: 10rpx; 
 | 
        } 
 | 
      } 
 | 
       
 | 
      &-add { 
 | 
        flex-direction: column; 
 | 
        color: $tn-content-color; 
 | 
        font-size: 26rpx; 
 | 
         
 | 
        &--icon { 
 | 
          font-size: 40rpx; 
 | 
        } 
 | 
         
 | 
        &__tips { 
 | 
          margin-top: 20rpx; 
 | 
          line-height: 40rpx; 
 | 
        } 
 | 
      } 
 | 
    } 
 | 
     
 | 
    &__add { 
 | 
      width: auto; 
 | 
      display: inline-block; 
 | 
       
 | 
      &--custom { 
 | 
        width: 100%; 
 | 
      } 
 | 
    } 
 | 
  } 
 | 
</style> 
 |