上传文件组件

100 阅读2分钟

<template>
  <div>
    <!-- 展示态 -->
    <div class="zk-form__display-text" v-if="_display">
      <slot name="tip"></slot>
      <div v-if="listType === 'text'" class="zk-upload-display-text">
        <template v-for="(file, index) in uploadList">
          <div
            v-if="file.uploadStatus === 'success'"
            :key="file.url + file.name + index"
            :class="`zk-upload--${file.uploadStatus}`">
            <zk-link
              :to="showLink ? file.url : 'javascript:;'"
              :target="showLink ? '_blank' : undefined"
              @click="preview(file, index)"
              :title="file.name">
              {{ file.name }}
            </zk-link>
          </div>
        </template>
      </div>
      <div v-else>
        <template v-for="(file, index) in uploadList">
          <zk-image
            v-if="file.uploadStatus === 'success'"
            :key="file.url + file.name + index"
            style="margin: 0 8px 0 0;"
            :src="file.url"
            :preview="false"
            v-bind="imageProps"
            @preview="preview(file, index)" />
        </template>
      </div>
    </div>


    <!-- 编辑态 -->
    <div v-else class="zk-upload" :class="{'zk-upload--disabled': uploadDisabled}">
      <template v-if="$slots.trigger">
        <span @click="triggerUpload">
          <slot name="trigger"></slot>
        </span>
        <slot></slot>
      </template>
      <!-- 上传按钮 -->
      <div v-else-if="triggerType === 'button'" class="zk-upload__button">
        <zk-button
          v-bind="uploadButton"
          :icon="uploadButton.icon || uploadIconClass"
          @click="triggerUpload"
          :disabled="uploadDisabled">
          {{ uploadButton.text || $t.tc('upload.fileListBtn') }}
        </zk-button>
        <slot></slot>
      </div>

      <!-- 上传拖拽区 -->
      <div
        v-else-if="triggerType === 'drag'"
        :style="previewStyle"
        class="zk-upload-drag zk-upload__preview--drag"
        @dragover.stop.prevent="dragoverHandler"
        @drop.stop.prevent="dropHandler"
        @click="triggerUpload">
        <div class="zk-upload-drag__content">
          <i v-if="previewContent.indexOf('icon') > -1" class="zk-upload-drag__add-icon" :class="uploadIconClass"></i>
          <div v-if="previewContent.indexOf('title') > -1" class="zk-upload-drag__title">
            <slot name="title">
              {{ title }}
            </slot>
          </div>
          <div v-if="previewContent.indexOf('tip') > -1" class="zk-upload-drag__tip">
            <slot name="tip">
              {{ tip }}
            </slot>
          </div>
        </div>
        <div class="zk-upload__finally" v-if="!listType && uploadList.length">
          <zk-image
            :src="uploadList[0].url"
            :preview="false"
            :deletable="!deleteDisabled"
            :show-preview-icon="showPreviewIcon"
            v-bind="imageProps"
            @preview="preview(uploadList[0], 0)"
            @delete="removeHandler(uploadList[0], 0)" />
        </div>
      </div>

      <!-- 上传列表 -->
      <ul v-if="listType === 'text' && uploadList.length && showFileList" class="zk-upload__filelist">
        <template>
          <li
            v-for="(file, index) in uploadList"
            :key="file.url + file.name + index"
            class="zk-upload__file"
            :class="`zk-upload--${file.uploadStatus}`">
            <div class="zk-upload__file-left">
              <slot name="imgBox" :fileName="file.name">
              </slot>
              <!-- <i class="zds-icon-attach"></i> -->
              <!-- <zk-link
                v-if="file.uploadStatus === 'success'"
                class="zk-upload__filename"
                :to="showLink ? file.url : 'javascript:;'"
                :target="showLink ? '_blank' : undefined"
                @click="preview(file, index)"
                :title="file.name">
                {{ file.name }}
              </zk-link> -->
              <span v-if="file.uploadStatus === 'success'"
                    class="zk-upload__filename">
                <span class="cursor" @click="downloadHandler(file)">{{ file.name }}</span>
              </span>
              <span v-else class="zk-upload__filename cursor" :title="file.name">{{ file.name }}</span>
            </div>
            <div class="zk-upload__file-right">
              <zk-progress
                :show-result-icon="true"
                type="circle" size="xs" :percentage="file.percentage"
                :status="file.uploadStatus">
              </zk-progress>
              <zk-button
                v-if="file.uploadStatus === 'exception'"
                shape="square" size="xs"
                icon="zds-icon-refresh"
                class="zk-upload__retry"
                @click="retryHandler(file, index)">
              </zk-button>
              <zk-button
                v-if="!deleteDisabled"
                shape="square" size="xs"
                icon="zds-icon-delete-outline"
                class="zk-upload__remove"
                @click="removeHandler(file, index)">
              </zk-button>
            </div>
          </li>
        </template>
      </ul>

      <!-- 图片列表上传 -->
      <div
        v-else-if="listType === 'picture' && showFileList"
        class="zk-upload-img"
        :style="{'marginTop': triggerType === 'picture' ? 0 : '8px'}">
        <div class="zk-upload-img__added"
             v-for="(file, index) in uploadList"
             :class="`zk-upload--${file.uploadStatus}`"
             :key="file.url + file.name + index"
             :style="imageProps">
          <template v-if="file.uploadStatus !== 'success'">
            <div>
              <zk-progress :show-result-icon="true"
                           type="circle" size="xs" :percentage="file.percentage"
                           :status="file.uploadStatus"></zk-progress>
            </div>
            <span class="zk-upload-img__filename">
              {{ file.uploadStatus === 'exception' ? $t.tc('upload.uploadFailed') : $t.tc('upload.uploading') }}
            </span>
            <div class="zk-upload-img__hover">
              <span v-if="file.uploadStatus === 'exception'">
                <i
                  class="zds-icon-refresh"
                  @click="retryHandler(file)"></i>
              </span>
              <span @click="removeHandler(file, index)">
                <i
                  class="zds-icon-delete-outline"></i>
              </span>
            </div>
          </template>
          <template v-else>
            <zk-image
              :src="file.url"
              :preview="false"
              :deletable="!deleteDisabled"
              :show-preview-icon="showPreviewIcon"
              v-bind="imageProps"
              @preview="preview(file, index)"
              @delete="removeHandler(file, index)" />
          </template>
        </div>
        <div
          v-if="uploadList.length < limit && triggerType === 'picture'"
          class="zk-upload-img__add"
          :style="imageProps"
          @click="triggerUpload">
          <i v-if="previewContent.indexOf('icon') > -1" class="zds-icon-add"></i>
          <slot v-if="previewContent.indexOf('title') > -1" name="title">
            {{ $t.tc('upload.imgBtn') }}
          </slot>
        </div>
      </div>

      <!-- 上传input控件 -->
      <input class="zk-upload__input"
             type="file"
             ref="input"
             :name="name"
             @change="changeHandler"
             :multiple="multiple"
             :accept="accept">
    </div>
  </div>
</template>

<script>
import axios from 'axios';
import formDisplay from 'zeekr-ui/src/mixins/form-display';

export default {
  name: 'BillingUpload',
  inject: {
    zkForm: {
      default: ''
    }
  },
  mixins: [formDisplay],
  props: {
    fileList: {
      type: Array,
      default: () => ([])
    },
    accept: {
      type: String,
      default: '*'
    },
    action: {
      type: String,
      default: 'https://gateway-pub-sit.zeekrlife.com/cloud-storage/putObject'
    },
    multiple: {
      type: Boolean,
      default: true
    },
    name: String,
    disabled: Boolean,
    limit: {
      type: Number,
      default: 99
    },
    sizeLimit: {
      type: Number,
      default: 102400 // kb
    },
    serviceConfig: {
      type: Object,
      default:() => ({
        prefix: 'zeekrui',
        serviceName: 'b516fa2c8d874cd38cf46b1ebbf31564',
        ossType: 'aliyun'
      })
    },
    triggerType: {
      type: String,
      default: 'button',
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ['button', 'picture', 'drag'].indexOf(value) !== -1;
      }
    },
    listType: {
      type: String,
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ['text', 'picture'].indexOf(value) !== -1;
      }
    },
    token: {
      type: String,
      default: ''
    },
    beforeUpload: {
      type: Function,
      default: null
    },
    headers: {
      type: Object,
      default: () => ({})
    },
    data: {
      type: Object
    },
    getUrl: Function,
    previewStyle: {
      type: Object,
      default: () => ({ width: '', height: '' })
    },
    previewContent: {
      type: Array,
      default: () => ['icon', 'title', 'tip']
    },
    uploadIconClass: {
      type: String,
      default: 'zds-icon-add'
    },
    fileKey: {
      type: String,
      default: 'file'
    },
    previewModal: {
      type: Boolean,
      default: true
    },
    showPreviewIcon: {
      type: Boolean,
      default: true
    },
    needTimestamp: {
      type: Boolean,
      default: true
    },
    uploadButton: {
      type: Object,
      default: () => ({})
    },
    showFileList: {
      type: Boolean,
      default: true
    },
    showLink: {
      type: Boolean,
      default: true
    },
    httpRequest: {
      type: Function,
      default: axios
    },
    previewProps: Object,
    imageProps: Object,
    quality: {
      type: Number,
      default: 0.5
    },
    compress: Boolean,
    beforeRemove: Function
  },
  computed: {
    uploadDisabled() {
      return this.deleteDisabled || this.uploadList.length >= this.limit;
    },
    deleteDisabled() {
      return (this.zkForm && this.zkForm.disabled) || this.disabled;
    },
    srcList() {
      return this.fileList.map(({ url }) => url);
    },
    previewTitle() {
      return this.fileList[this.previewCurrent]?.name;
    }
  },
  watch: {
    fileList (val) {
      this.uploadList = val.map((file) => {
        if (file.url) {
          return {
            name: file.name,
            url: file.url,
            uploadStatus: 'success',
            percentage: 100,
            ...file
          };
        }
        return file;

      });
    }
  },
  data () {
    return {
      uploadList: this.fileList.map((file) => ({
        name: file.name,
        url: file.url,
        uploadStatus: 'success',
        percentage: 100,
        ...file
      })),
      visible: false,
      previewImg: {
        name: '',
        url: ''
      },
      title: this.$t.tc('upload.title'),
      tip: '',
      defaultCurrent: 0, // 预览图片默认序号
      previewCurrent: 0
    };
  },
  methods: {
    messageHandler(type = 'warning', message) {
      this.$message({
        type,
        message,
      });
    },
    changeHandler(e) {
      const { files } = e.target;
      this.dealFiles(files);
    },
    async dealFiles(files) {
      if ((this.uploadList.length + files.length) > this.limit) {
        this.messageHandler('error', this.$t.tc('upload.overLength'));
        this.$emit('on-limit', files);
        return;
      }
      if (this.beforeUpload && typeof this.beforeUpload === 'function') {
        const res = this.beforeUpload(files);
        if (typeof res === 'boolean' && !res) return;
        if (res instanceof Promise) {
          try {
            files = await res;
          } catch (e) {
            return;
          }
        } else {
          files = res;
        }
      }
      if (files && files.length > 0) {
        for (let i = 0, len = files.length; i < len; i++) {
          const file = files[i];
          const { size } = file;
          file.percentage = 0;
          if (size / 1024 > this.sizeLimit) {
            this.$emit('on-size-limit', file);
            return this.messageHandler('error', `${file.name} ${this.$t.tc('upload.overSize')}`);
          }
          this.uploadList.push(file);
          this.upload(file, this.uploadList.length - 1);
        }
      }
    },
    triggerUpload() {
      if (this.uploadDisabled) return;
      this.$refs.input.value = null;
      this.$refs.input.click();
    },
    request (formData, progressCb) {
      const url = this.needTimestamp
        ? `${this.action}?_t=${Date.parse(new Date())}` : this.action;
      let headers = {
        'Content-Type': 'multipart/form-data',
        'Cache-Control': 'no-cache',
        ...this.headers
      };
      if (this.token) {
        headers.Authorization = this.token;
      }
      return this.httpRequest({
        method: 'post',
        url,
        headers,
        params: this.serviceConfig,
        data: formData,
        onUploadProgress: (progressEvent) => {
          const { total, loaded } = progressEvent;
          /*  eslint-disable-next-line  */
          const percentage = Math.ceil(loaded / total * 100);
          this.$emit('on-progress', percentage, progressEvent);
          progressCb(percentage);
        },
      });
    },
    async upload (file, fileIndex) {
      let formData = new FormData();
      const { name } = file;
      formData.append(this.fileKey, file, name);
      if (this.data) { // 如果有自定义参数
        let { data } = this;
        if (typeof data === 'function') {
          data = this.data();
        }
        Object.keys(data).forEach((key) => {
          formData.append(key, this.data[key]);
        });
      }
      this.request(formData, (percentage) => {
        if (percentage === 100) return;
        file.percentage = percentage;
        this.$forceUpdate();
      }).then(async (res) => {
        if (res.status === 200 && (res.data.data || res.data.data?.url)) {
          let url;
          let fileList = [];
          const { data } = res;
          if (this.getUrl) {
            url = await this.getUrl(data);
          } else {
            url = typeof data.data === 'string' ? data.data : data.data?.url;
          }
          if (url) {
            file.url = url;
            file.percentage = 100;
            file.uploadStatus = 'success';
            this.$forceUpdate();
            fileList = this.updateFileList();
          }
          this.$emit('on-success', data.data, file, fileList, fileIndex, data);
        } else {
          throw res.data;
        }
      }).catch((err) => {
        file.uploadStatus = 'exception';
        this.$forceUpdate();
        this.$emit('on-error', err, file, this.fileList, fileIndex);
      });
    },
    async removeHandler(file, index) {
      if (this.beforeRemove) {
        try {
          const res = await this.beforeRemove(file, index);
          if (res === false) {
            return;
          }
        } catch {
          return;
        }
      }
      this.uploadList.splice(index, 1);
      // 如果是删除上传失败的项,不用更新外部的fileList
      file.url && this.$emit('update:fileList', this.uploadList);
      this.$emit('on-remove', file, index);
    },
    downloadHandler(file) {
      this.$emit('onDownload', file);
    },
    preview(file, index) {
      if (this.showLink && this.listType === 'text') return;
      if (this.previewModal) {
        this.defaultCurrent = index;
        this.visible = true;
      }
      this.$emit('on-preview', file, this.fileList, index);
    },
    updateFileList() {
      let fileList = [];
      for (let i = 0, len = this.uploadList.length; i < len; i++) {
        const file = this.uploadList[i];
        if (file.uploadStatus === 'success') {
          const { name, url } = file;
          fileList.push({
            name,
            url,
            ...file
          });
        } else {
          fileList.push(file);
        }
      }
      this.$emit('update:fileList', fileList);
      return fileList;
    },
    dragoverHandler(e) {
      if (this.uploadDisabled) return;
      e.dataTransfer.dropEffect = 'copy';
    },
    dropHandler(e) {
      if (this.uploadDisabled) return;
      let filterFiles = [];
      const { files } = e.dataTransfer;
      if (this.accept === '*') {
        filterFiles = files;
      } else {
        // 过滤格式不符合的文件
        for (let i = 0, len = files.length; i < len; i++) {
          const file = files[i];
          const arr = file.name.split('.');
          const ext = arr[arr.length - 1];
          const accepts = this.accept.split(',');
          let acceptFlag = false;
          /*  eslint-disable-next-line  */
          for (let i = 0, len = accepts.length; i < len; i++) {
            const reg = new RegExp(accepts[i]); // 正则判断image/*, text/* 这种写法
            if (reg.test(file.type)) {
              acceptFlag = true;
              break;
            }
          }
          if (this.accept.includes(ext) || acceptFlag) {
            filterFiles.push(file);
          }
        }
      }
      this.dealFiles(filterFiles);
    },
    retryHandler(file, fileIndex) {
      file.percentage = 0;
      this.$forceUpdate();
      this.upload(file, fileIndex);
    }
  }
};
</script>
<style scoped>
.file-img {
  width: 16px;
  vertical-align: middle;
  margin-right: 4px;
}
.file-name {
  vertical-align: middle;
}
.cursor {
  cursor: pointer;
}
</style>