<template> 
 | 
  <view v-if="!disabled" class="tn-image-upload-class tn-image-upload"> 
 | 
    <movable-area 
 | 
      class="tn-image-upload__movable-area" 
 | 
      :style="{ 
 | 
        height: movableAreaHeight 
 | 
      }" 
 | 
      @mouseenter="mouseEnterArea" 
 | 
      @mouseleave="mouseLeaveArea" 
 | 
    > 
 | 
      <block 
 | 
        v-for="(item, index) in lists" 
 | 
        :key="item.id" 
 | 
      > 
 | 
        <movable-view 
 | 
          class="tn-image-upload__movable-view" 
 | 
          :style="{ 
 | 
            width: $t.string.getLengthUnitValue(width), 
 | 
            height: $t.string.getLengthUnitValue(height), 
 | 
            zIndex: item.zIndex, 
 | 
            opacity: item.opacity, 
 | 
             
 | 
          }" 
 | 
          :x="item.x" 
 | 
          :y="item.y" 
 | 
          direction="all" 
 | 
          :damping="40" 
 | 
          :disabled="item.disabled" 
 | 
          @change="movableChange($event, item)" 
 | 
          @touchstart="movableStart(item)" 
 | 
          @mousedown="movableStart(item)" 
 | 
          @touchend="movableEnd(item)" 
 | 
          @mouseup="movableEnd(item)" 
 | 
          @longpress="movableLongPress(item)" 
 | 
        > 
 | 
          <view 
 | 
            class="tn-image-upload__item tn-image-upload__item-preview" 
 | 
            :style="{ 
 | 
              width: $t.string.getLengthUnitValue(itemWidth, 'px'), 
 | 
              height: $t.string.getLengthUnitValue(itemHeight, 'px'), 
 | 
              transform: `scale(${item.scale})` 
 | 
            }" 
 | 
          > 
 | 
            <!-- 删除按钮 --> 
 | 
            <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.data.progress > 0 && !item.data.error" 
 | 
              class="tn-image-upload__item-preview__progress" 
 | 
              :percent="item.data.progress" 
 | 
              :showPercent="false" 
 | 
              :round="false" 
 | 
              :height="8" 
 | 
            ></tn-line-progress> 
 | 
            <!-- 重试按钮 --> 
 | 
            <view v-if="item.data.error" class="tn-image-upload__item-preview__error-btn" @tap.stop="retry(index)">点击重试</view> 
 | 
            <!-- 图片信息 --> 
 | 
            <image 
 | 
              class="tn-image-upload__item-preview__image" 
 | 
              :src="item.data.url || item.data.path" 
 | 
              :mode="imageMode" 
 | 
              @tap.stop="doPreviewImage(item.data.url || item.data.path, index)" 
 | 
            ></image> 
 | 
          </view> 
 | 
        </movable-view> 
 | 
      </block> 
 | 
       
 | 
      <!-- 添加按钮 --> 
 | 
      <view 
 | 
        v-if="maxCount > lists.length" 
 | 
        class="tn-image-upload__add" 
 | 
        :style="{ 
 | 
          top: addBtn.y + 'px', 
 | 
          left: addBtn.x + 'px', 
 | 
          width: $t.string.getLengthUnitValue(itemWidth, 'px'), 
 | 
          height: $t.string.getLengthUnitValue(itemHeight, 'px') 
 | 
        }" 
 | 
        @tap="selectFile" 
 | 
      > 
 | 
        <!-- 添加按钮 --> 
 | 
        <view 
 | 
          class="tn-image-upload__item tn-image-upload__item-add" 
 | 
          hover-class="tn-hover-class" 
 | 
          hover-stay-time="150" 
 | 
          :style="{ 
 | 
            width: $t.string.getLengthUnitValue(itemWidth, 'px'), 
 | 
            height: $t.string.getLengthUnitValue(itemHeight, 'px') 
 | 
          }" 
 | 
        > 
 | 
          <view class="tn-image-upload__item-add--icon tn-icon-add"></view> 
 | 
          <view class="tn-image-upload__item-add__tips">{{ uploadText }}</view> 
 | 
        </view> 
 | 
      </view> 
 | 
    </movable-area> 
 | 
  </view> 
 | 
</template> 
 | 
  
 | 
<script> 
 | 
  export default { 
 | 
    name: 'tn-image-upload-drag', 
 | 
    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 
 | 
      }, 
 | 
      // 预览上传图片的裁剪模式 
 | 
      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 
 | 
      }, 
 | 
      // 预览图片和选择图片区域的宽度 
 | 
      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: { 
 | 
      movableAreaHeight() { 
 | 
        if (this.lists.length < this.maxCount) { 
 | 
          return Math.ceil((this.lists.length + 1) / this.baseData.columns) * uni.upx2px(this.height) + 'px' 
 | 
        } else { 
 | 
          return Math.ceil(this.lists.length / this.baseData.columns) * uni.upx2px(this.height) + 'px' 
 | 
        } 
 | 
      }, 
 | 
      itemWidth() { 
 | 
        return uni.upx2px(this.width) - (uni.upx2px(10) * 2) 
 | 
      }, 
 | 
      itemHeight() { 
 | 
        return uni.upx2px(this.height) - (uni.upx2px(10) * 2) 
 | 
      } 
 | 
    }, 
 | 
    data() { 
 | 
      return { 
 | 
        lists: [], 
 | 
        uploading: false, 
 | 
        baseData: { 
 | 
          windowWidth: 0, 
 | 
          columns: 0, 
 | 
          dragItem: null, 
 | 
          widthPx: 0, 
 | 
          heightPx: 0 
 | 
        }, 
 | 
        addBtn: { 
 | 
          x: 0, 
 | 
          y: 0 
 | 
        }, 
 | 
        timer: null, 
 | 
        dragging: 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', this.sortList(), this.index) 
 | 
      } 
 | 
    }, 
 | 
    created() { 
 | 
      this.baseData.windowWidth = uni.getSystemInfoSync().windowWidth 
 | 
    }, 
 | 
    mounted() { 
 | 
      this.$nextTick(() => { 
 | 
        this.updateDragInfo() 
 | 
      }) 
 | 
    }, 
 | 
    methods: { 
 | 
      // 清除列表 
 | 
      clear() { 
 | 
        this.lists = [] 
 | 
        this.updateAddBtnPositioin() 
 | 
      }, 
 | 
      // 重新上传队列中上传失败所有文件 
 | 
      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, this.sortList(), this.index) 
 | 
              this.showToast('超出可允许文件大小') 
 | 
            } else { 
 | 
              if (maxCount <= lists.length) { 
 | 
                this.$emit('on-exceed', val, this.sortList(), this.index) 
 | 
                this.showToast('超出最大允许的文件数') 
 | 
                return 
 | 
              } 
 | 
              lists.push(this.handleDragListItem({ 
 | 
                url: val.path, 
 | 
                progress: 0, 
 | 
                error: false, 
 | 
                file: val 
 | 
              })) 
 | 
              this.updateAddBtnPositioin() 
 | 
            } 
 | 
          }) 
 | 
          this.$emit('on-choose-complete', this.sortList(), 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].data.progress = 0 
 | 
        this.lists[index].data.error = false 
 | 
        this.lists[index].data.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.sortList(), this.index) 
 | 
          return 
 | 
        } 
 | 
        // 检查是否已经全部上传或者上传中 
 | 
        if (this.lists[index].data.progress === 100) { 
 | 
          this.lists[index].data.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].data.error = false 
 | 
        this.uploading = true 
 | 
        // 创建上传对象 
 | 
        const task = uni.uploadFile({ 
 | 
          url: this.action, 
 | 
          filePath: this.lists[index].data.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].data.response = data 
 | 
              this.lists[index].data.progress = 100 
 | 
              this.lists[index].data.error = false 
 | 
              this.$emit('on-success', data, index, this.sortList(), 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.sortList(), this.index) 
 | 
          } 
 | 
        }) 
 | 
        this.lists[index].uploadTask = task 
 | 
        task.onProgressUpdate(res => { 
 | 
          if (res.progress > 0) { 
 | 
            this.lists[index].data.progress = res.progress 
 | 
            this.$emit('on-progress', res, index, this.sortList(), this.index) 
 | 
          } 
 | 
        }) 
 | 
      }, 
 | 
      // 上传失败 
 | 
      uploadError(index, err) { 
 | 
        this.lists[index].data.progress = 0 
 | 
        this.lists[index].data.error = true 
 | 
        this.lists[index].data.response = null 
 | 
        this.showToast('上传失败,请重试') 
 | 
        this.$emit('on-error', err, index, this.sortList(), 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].data.progress < 100 && this.lists[index].data.progress > 0) { 
 | 
          typeof this.lists[index].data.uploadTask !== 'undefined' && this.lists[index].data.uploadTask.abort() 
 | 
        } 
 | 
        this.remove(index) 
 | 
        this.$forceUpdate() 
 | 
        this.$emit('on-remove', index, this.sortList(), this.index) 
 | 
        this.showToast('删除成功') 
 | 
      }, 
 | 
      // 移除文件,通过ref手动形式进行调用 
 | 
      remove(index) { 
 | 
        if (!this.deleteable) return 
 | 
        // 判断索引合法 
 | 
        if (index >= 0 && index < this.lists.length) { 
 | 
          let currentItemIndex = this.lists[index].index 
 | 
          this.lists.splice(index, 1) 
 | 
          // 重新排列列表信息 
 | 
          for (let item of this.lists) { 
 | 
            if (item.index > currentItemIndex) { 
 | 
              item.index -= 1 
 | 
              item.x = item.positionX * this.baseData.widthPx 
 | 
              item.y = item.positionY * this.baseData.heightPx 
 | 
              item.positionX = item.index % this.baseData.columns 
 | 
              item.positionY = Math.floor(item.index / this.baseData.columns) 
 | 
              this.$nextTick(() => { 
 | 
                item.x = item.positionX * this.baseData.widthPx 
 | 
                item.y = item.positionY * this.baseData.heightPx 
 | 
              }) 
 | 
            } 
 | 
          } 
 | 
           
 | 
          this.updateAddBtnPositioin() 
 | 
        } 
 | 
      }, 
 | 
      // 预览图片 
 | 
      doPreviewImage(url, index) { 
 | 
        if (!this.previewFullImage) return 
 | 
        const images = this.lists.sort((l1, l2) => { return l1.index - l2.index}).map(item => item.data.url || item.data.path) 
 | 
        uni.previewImage({ 
 | 
          urls: images, 
 | 
          current: url, 
 | 
          success: () => { 
 | 
            this.$emit('on-preview', url, this.sortList(), 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 
 | 
      }, 
 | 
       
 | 
      /********************* 拖拽处理 ********************/ 
 | 
       
 | 
      // 更新拖拽信息 
 | 
      updateDragInfo() { 
 | 
        this.baseData.widthPx = uni.upx2px(this.width) 
 | 
        this.baseData.heightPx = uni.upx2px(this.height) 
 | 
         
 | 
        // 获取可移动区域的信息,用于判断当前有多少列 
 | 
        const query = uni.createSelectorQuery().in(this) 
 | 
        query.select('.tn-image-upload__movable-area').boundingClientRect() 
 | 
        query.exec((res) => { 
 | 
          if (!res) { 
 | 
            setTimeout(() => { 
 | 
              this.updateDragInfo() 
 | 
            }, 10) 
 | 
            return 
 | 
          } 
 | 
          this.baseData.columns = Math.floor(res[0].width / this.baseData.widthPx) 
 | 
           
 | 
          // 初始化可拖拽列表信息 
 | 
          this.lists = [] 
 | 
          this.fileList.forEach((item) => { 
 | 
            // 首先检查内部是否已经添加过这张图片,因为外部绑定了一个对象给fileList的话(对象引用),进行修改外部fileList时, 
 | 
            // 会触发watch,导致重新把原来的图片再次添加到this.lists 
 | 
            // 数组的some方法意思是,只要数组元素有任意一个元素条件符合,就返回true,而另一个数组的every方法的意思是数组所有元素都符合条件才返回true 
 | 
            let tmp = this.lists.map(value => { 
 | 
              return value.data 
 | 
            }).some(listVal => { 
 | 
              return listVal.url === item.url 
 | 
            }) 
 | 
            // 如果内部没有这张图片,则添加到内部 
 | 
            !tmp && this.lists.push(this.handleDragListItem({ 
 | 
              url: item.url, 
 | 
              error: false, 
 | 
              progress: 100 
 | 
            })) 
 | 
          }) 
 | 
           
 | 
          // 更新添加按钮位置 
 | 
          this.updateAddBtnPositioin() 
 | 
        }) 
 | 
      }, 
 | 
      // 处理拖拽列表信息 
 | 
      handleDragListItem(item) { 
 | 
        const positionX = this.lists.length % this.baseData.columns 
 | 
        const positionY = Math.floor(this.lists.length / this.baseData.columns) 
 | 
        const x = positionX * this.baseData.widthPx 
 | 
        const y = positionY * this.baseData.heightPx 
 | 
        return { 
 | 
          id: this.unique(), 
 | 
          x, 
 | 
          y, 
 | 
          preX: x, 
 | 
          preY: y, 
 | 
          positionX, 
 | 
          positionY, 
 | 
          zIndex:1, 
 | 
          disabled: true, 
 | 
          opacity: 1, 
 | 
          scale: 1, 
 | 
          index: this.lists.length, 
 | 
          offset: 0, 
 | 
          moveEnd: false, 
 | 
          moving: false, 
 | 
          data: { 
 | 
            ...item 
 | 
          } 
 | 
        } 
 | 
      }, 
 | 
      // 生成元素唯一id 
 | 
      unique(n = 6) { 
 | 
        let id = '' 
 | 
        for (let i = 0; i < n; i++) id += Math.floor(Math.random() * 10) 
 | 
        return 'tn_' + new Date().getTime() + id 
 | 
      }, 
 | 
      // 更新添加按钮位置 
 | 
      updateAddBtnPositioin() { 
 | 
        if (this.lists.length >= this.maxCount) return 
 | 
         
 | 
        this.addBtn.x = (this.lists.length % this.baseData.columns) * this.baseData.widthPx 
 | 
        this.addBtn.y = Math.floor(this.lists.length / this.baseData.columns) * this.baseData.heightPx 
 | 
      }, 
 | 
      // 获取排序后数据 
 | 
      sortList() { 
 | 
        const list = this.lists.slice() 
 | 
        list.sort((l1, l2) => { 
 | 
          return l1.index - l2.index 
 | 
        }) 
 | 
        return list.map(item => { 
 | 
          return item.data 
 | 
        }) 
 | 
      }, 
 | 
      mouseEnterArea () { 
 | 
        // #ifdef H5 
 | 
        this.lists.forEach(item => { 
 | 
          item.disabled = false 
 | 
        }) 
 | 
        // #endif 
 | 
      }, 
 | 
      mouseLeaveArea () { 
 | 
        // #ifdef H5 
 | 
        if (this.baseData.dragItem) { 
 | 
          this.lists.forEach(item => { 
 | 
            item.disabled = true 
 | 
            item.zIndex = 1 
 | 
            item.offset = 0 
 | 
            item.moveEnd = true 
 | 
            if (item.id === this.baseData.dragItem.id) { 
 | 
              if (this.timer) { 
 | 
                clearTimeout(this.timer) 
 | 
                this.timer = null 
 | 
              } 
 | 
              item.x = item.preX 
 | 
              item.y = item.preY 
 | 
              this.$nextTick(() => { 
 | 
                item.x = item.positionX * this.baseData.widthPx 
 | 
                item.y = item.positionY * this.baseData.heightPx 
 | 
                this.baseData.dragItem = null 
 | 
              }) 
 | 
            } 
 | 
          }) 
 | 
          this.dragging = false 
 | 
        } 
 | 
        // #endif 
 | 
      }, 
 | 
      movableLongPress(item) { 
 | 
        // #ifndef H5 
 | 
          uni.vibrateShort() 
 | 
          // console.log("LongPress--------------------------------------------------------------"); 
 | 
          this.lists.forEach(value => { 
 | 
            value.moving = false 
 | 
          }) 
 | 
          this.dragging = true 
 | 
          // 设置对应的元素允许拖动 
 | 
          const index = this.lists.findIndex(obj => { 
 | 
            return obj.id === item.id 
 | 
          }) 
 | 
          item.disabled = false 
 | 
          item.opacity = 0.7 
 | 
          item.scale = 1.1 
 | 
          this.$set(this.lists, index, item) 
 | 
        // #endif 
 | 
      }, 
 | 
      movableChange (e, item) { 
 | 
        if (!item || !this.dragging) return 
 | 
        // console.log("movableChange"); 
 | 
        item.moving = true 
 | 
        item.preX = e.detail.x 
 | 
        item.preY = e.detail.y 
 | 
        // console.log(item.preX, item.preY); 
 | 
         
 | 
        if (e.detail.source === 'touch') { 
 | 
          if (!item.moveEnd) { 
 | 
            item.offset = Math.sqrt( 
 | 
              Math.pow(item.preX - item.positionX * this.baseData.widthPx, 2) +  
 | 
              Math.pow(item.preY - item.positionY * this.baseData.heightPx, 2)) 
 | 
          } 
 | 
          let x = Math.floor((e.detail.x + this.baseData.widthPx / 2) / this.baseData.widthPx) 
 | 
          if (x > this.baseData.columns) return 
 | 
          let y = Math.floor((e.detail.y + this.baseData.heightPx / 2) / this.baseData.heightPx) 
 | 
          let index = this.baseData.columns * y + x 
 | 
          if (item.index !== index && index < this.lists.length) { 
 | 
            for (let obj of this.lists) { 
 | 
              if (item.index > index && obj.index >= index && obj.index < item.index) { 
 | 
                this.updateItemPosition(obj, 1) 
 | 
              } else if (item.index < index && obj.index <= index && obj.index > item.index) { 
 | 
                this.updateItemPosition(obj, -1) 
 | 
              } else if (item.id != obj.id) { 
 | 
                // obj.offset = 0 
 | 
                // console.log(obj.moving); 
 | 
                // if (!obj.moving) { 
 | 
                //   obj.preX = obj.positionX * this.baseData.widthPx 
 | 
                //   obj.preY = obj.positionY * this.baseData.heightPx 
 | 
                //   console.log("moving", obj.id, obj.preX, obj.preY); 
 | 
                // } 
 | 
                // obj.x = obj.preX 
 | 
                // obj.y = obj.preY 
 | 
                // // console.log(obj.id, obj.preX, obj.preY); 
 | 
                // this.$nextTick(() => { 
 | 
                //   obj.x = obj.positionX * this.baseData.widthPx 
 | 
                //   obj.y = obj.positionY * this.baseData.heightPx 
 | 
                // }) 
 | 
              } 
 | 
            } 
 | 
            item.index = index 
 | 
            item.positionX = x 
 | 
            item.positionY = y 
 | 
            // TODO 发送事件 
 | 
          } 
 | 
        } 
 | 
      }, 
 | 
      movableStart (item) { 
 | 
        // console.log("movableStart"); 
 | 
        this.lists.forEach(item => { 
 | 
          item.zIndex = 1 
 | 
          // #ifdef H5 
 | 
          item.disabled = false 
 | 
          // #endif 
 | 
        }) 
 | 
        item.zIndex = 10 
 | 
        item.moveEnd = false 
 | 
        this.baseData.dragItem = item 
 | 
        // #ifdef H5 
 | 
        this.dragging =true 
 | 
        this.timer = setTimeout(() => { 
 | 
          item.opacity = 0.7 
 | 
          item.scale = 1.1 
 | 
          clearTimeout(this.timer) 
 | 
          this.timer = null 
 | 
        }, 200) 
 | 
        // #endif 
 | 
      }, 
 | 
      movableEnd (item) { 
 | 
        if (!this.dragging) return 
 | 
        // console.log("movableEnd"); 
 | 
        const index = this.lists.findIndex(obj => { 
 | 
          return obj.id === item.id 
 | 
        }) 
 | 
        if (!item.moving) { 
 | 
          item.preX = item.positionX * this.baseData.widthPx 
 | 
          item.preY = item.positionY * this.baseData.heightPx 
 | 
        } 
 | 
        item.x = item.preX 
 | 
        item.y = item.preY 
 | 
        item.offset = 0 
 | 
        item.moveEnd = true 
 | 
        item.moving = false 
 | 
        item.disabled = true 
 | 
        // console.log(item.x, item.y); 
 | 
        // console.log(item.id, item.moving); 
 | 
        // this.$set(this.lists, index, item) 
 | 
        // this.lists[index] = item 
 | 
        // console.log(this.lists[index]); 
 | 
        this.lists.forEach(listValue => { 
 | 
          listValue.moving = false 
 | 
          listValue.disabled = true 
 | 
        }) 
 | 
        this.$nextTick(() => { 
 | 
          item.x = item.positionX * this.baseData.widthPx 
 | 
          item.y = item.positionY * this.baseData.heightPx 
 | 
          item.opacity = 1 
 | 
          item.scale = 1 
 | 
          this.baseData.dragItem = null 
 | 
          this.dragging = false 
 | 
          // console.log(item.x, item.y); 
 | 
          this.$set(this.lists, index, item) 
 | 
        }) 
 | 
        this.$emit('sort-list', this.sortList()) 
 | 
      }, 
 | 
      // 更新图片位置信息 
 | 
      updateItemPosition(item, offset) { 
 | 
        const index = this.lists.findIndex(obj => { 
 | 
          return obj.id === item.id 
 | 
        }) 
 | 
        item.index += offset 
 | 
        item.offset = 0 
 | 
        item.positionX = item.index % this.baseData.columns 
 | 
        item.positionY = Math.floor(item.index / this.baseData.columns) 
 | 
        if (!item.moving) { 
 | 
          item.preX = item.positionX * this.baseData.widthPx 
 | 
          item.preY = item.positionY * this.baseData.heightPx 
 | 
        } 
 | 
        item.x = item.preX 
 | 
        item.y = item.preY 
 | 
        // console.log("updateItemPosition", item.id, item.preX, item.preY); 
 | 
        // this.$set(this.lists, index, item) 
 | 
        this.$nextTick(() => { 
 | 
          item.x = item.positionX * this.baseData.widthPx 
 | 
          item.y = item.positionY * this.baseData.heightPx 
 | 
          this.$set(this.lists, index, item) 
 | 
        }) 
 | 
      } 
 | 
    } 
 | 
  } 
 | 
</script> 
 | 
  
 | 
<style lang="scss" scoped> 
 | 
   
 | 
  .tn-image-upload { 
 | 
    position: relative; 
 | 
     
 | 
    &__movable-area { 
 | 
      width: 100%; 
 | 
    } 
 | 
     
 | 
    &__movable-view { 
 | 
      overflow: hidden; 
 | 
    } 
 | 
     
 | 
    &__item { 
 | 
      /* #ifndef APP-NVUE */ 
 | 
      display: flex; 
 | 
      /* #endif */ 
 | 
      align-items: center; 
 | 
      justify-content: center; 
 | 
      width: 200rpx; 
 | 
      height: 200rpx; 
 | 
      background-color: transparent; 
 | 
      position: relative; 
 | 
      border-radius: 5rpx; 
 | 
      overflow: hidden; 
 | 
       
 | 
      &-preview { 
 | 
        border: 1rpx solid $tn-space-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: rgba(0,0,0,0.1); 
 | 
          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: rgba(0,0,0,0.5); 
 | 
          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 { 
 | 
      background-color: $tn-space-color; 
 | 
      position: absolute; 
 | 
      // border-radius: 10rpx; 
 | 
      // margin: 10rpx; 
 | 
      // margin-left: 0; 
 | 
    } 
 | 
  } 
 | 
</style> 
 |