基于elementui开发上传组件,实现文件上传loading列表

一、效果图

实现上传按钮或者其他元素的popover显示上传文件的状态,速度,大小等信息
在这里插入图片描述
在这里插入图片描述

优点

  1. 能够直观看到文件上传的进度和状态
  2. 优化了大文件上传的时候,系统大面积loading空白页过长,没有用户反馈
  3. 可扩展性强,可以增加失败后重新上传,自定义上传方法等
  4. 可以传参限制文件大小,文件类型,不用自行写校验规则,依旧可以继续使用自定义方法

二、使用

直接使用

此时使用,会默认使用一个el-button作为上传触发
			<ProgUpPopover
              v-if="!setReadOnly"
              :uploadAction="uploadAction"
              multiple
              :data="uploadData"
              accept=".png, .jpg"
              btnText="上传图片"
              :onSuccess="handleUploadSuccess"
            ></ProgUpPopover>

插槽使用

使用customTrigger插槽来自定义触发的组件,以便来满足其他业务需求
			<ProgUpPopover
              v-if="!setReadOnly"
              :uploadAction="uploadAction"
              multiple
              :data="uploadData"
              accept=".png, .jpg"
              btnText="上传图片"
              :onSuccess="handleUploadSuccess"
            >
           	 <template #customTrigger>
            <el-button type="primary" size="small" icon="el-icon-upload"> test </el-button>
         	 </template>
			</ProgUpPopover>

三、源码

上传组件ProgUpPopover.vue

<template>
  <div class="ax-upload-container">
    <el-upload
      class="upload_btn"
      :action="uploadActionComputed"
      :data="data"
      :headers="headersComputed"
      :on-success="handleUploadSuccess"
      :show-file-list="false"
      :multiple="multiple"
      :on-error="handleUploadError"
      :on-change="handleInputChange"
      :on-progress="handleProgress"
      :accept="accept"
      :before-upload="handleBeforeUpload"
    >
      <el-popover placement="bottom" trigger="hover">
        <UploadCard :uploadFileList="uploadFileList" :hasWaitingOrUploading="hasWaitingOrUploading"></UploadCard>

        <slot name="customTrigger" slot="reference" v-if="customTriggerVisiable"></slot>
        <el-button v-else type="primary" slot="reference" size="small" :icon="BtnIcon"> {{ btnText }}</el-button>
      </el-popover>
    </el-upload>
  </div>
</template>
<script>
import { FileState, TableUtils } from './TableUtil';
import UploadCard from './UploadCard.vue';

export default {
  components: {
    UploadCard,
  },
  props: {
    uploadAction: {
      type: String,
      default: '',
    },
    uploadBtnIcon: {
      type: String,
      default: 'el-icon-upload',
    },
    fileSizeLimit: {
      type: String,
      default: '150MB',
    },
    headers: {
      type: Object,
      default: null,
    },
    accept: {
      type: String,
      default: null,
    },
    data: {
      type: Object,
      default: null,
    },
    btnText: {
      type: String,
      default: '上传',
    },
    onSuccess: {
      type: Function,
      default: null,
    },
    onError: {
      type: Function,
      default: null,
    },
    beforeUpload: {
      type: Function,
      default: null,
    },
    onChange: {
      type: Function,
      default: null,
    },
    onProgress: {
      type: Function,
      default: null,
    },
    multiple: {
      type: Boolean,
      default: false,
    },
  },
  computed: {
    uploadActionComputed() {
      return this.uploadAction
        ? `${window.__HOST__URL__ + window.__PREFIX__URL__}${this.uploadAction}`
        : `${
            window.__HOST__URL__ + window.__PREFIX__URL__
          }sjwflowCommProd/cap/projectImport/importIqpFlowZipToOneQueryForm`;
    },
    headersComputed() {
      return this.headers ? this.headers : { auth: this.$store.getters.token };
    },
  },
  mounted() {
    this.customTriggerVisiable = this.$scopedSlots.customTrigger;
    this.BtnIcon = this.uploadBtnIcon;
  },
  data() {
    return {
      customTriggerVisiable: false,
      // 上传文件列表
      uploadFileList: [],
      BtnIcon: '',
      hasWaitingOrUploading: false,
    };
  },
  methods: {
    getBtnIcon(uploadFileList) {
      this.hasWaitingOrUploading = false;
      let hasError = false;

      for (let i = 0; i < uploadFileList.length; i += 1) {
        const file = uploadFileList[i];

        if (file.state === FileState.Waiting || file.state === FileState.Uploading) {
          this.hasWaitingOrUploading = true;
        }

        if (file.state === FileState.Error) {
          hasError = true;
          break;
        }
      }

      if (this.hasWaitingOrUploading) {
        this.$emit('setIcon', 'el-icon-loading');
        return 'el-icon-loading';
      }
      this.$emit('setIcon', this.uploadBtnIcon);
      return this.uploadBtnIcon;
    },
    handleUploadSuccess(response, file, fileList) {
      if (this.onSuccess) {
        this.onSuccess(response, file, fileList);
      }
      const index = this.uploadFileList.findIndex(item => {
        return file.uid === item.id;
      });
      if (response.code === 200) {
        if (index === -1) {
          //   this.uploadFileList.push({
          //     file,
          //     name: file.name,
          //     state: FileState.Waiting,
          //     progress: 0,
          //     size: TableUtils.formatFileSize(file.size),
          //     speed: '速度计算中...',
          //     id: file.uid,
          //   });
        } else {
          this.uploadFileList[index].state = FileState.Success;
        }
        if (!this.onSuccess) {
          this.$message({
            type: 'success',
            message: '上传成功',
          });
        }
      } else {
        if (!this.onSuccess) {
          this.$message({
            type: 'warning',
            message: response.message,
          });
        }
        if (index !== -1) {
          this.uploadFileList[index].state = FileState.Error;
        }
      }
    },
    handleUploadError(err, file, fileList) {
      // 上传失败的回调

      const index = this.uploadFileList.findIndex(item => {
        return file.uid === item.id;
      });

      if (index !== -1) {
        this.uploadFileList[index].state = FileState.Error;
      }
      if (this.onError) {
        this.onError(err, file, fileList);
      } else {
        this.$message({
          type: 'Error',
          message: err,
        });
      }
    },
      // 检查文件格式
    checkAccept(file) {
      // 获取文件名
      const fileName = file.name;
      // 判断是否有接受参数
      if (this.accept) {
        // 将接受参数转换为数组
        const arr = this.accept.split(",");
        const lowerCaseArray = arr.map(element => element.toLowerCase());

        // 获取文件后缀
        const index = fileName.lastIndexOf(".");

        // 判断文件后缀是否在接受参数中
        const type = lowerCaseArray.includes(fileName.slice(index).toLowerCase());
        // 如果不存在,则报错
        if (!type) {
          this.$message.error(`请上传${this.accept}文件`);
          return false;
        }
        // 如果存在,则返回true
        return true;
      }
      // 如果没有接受参数,则直接返回true
      return true;
    },
    // 检查文件尺寸
    checkFileSize(file) {
      const sizeLimit = this.parseSize(this.fileSizeLimit);

      if (file.size > sizeLimit) {
        this.$message.error(`上传文件大小请小于${this.fileSizeLimit}`);
        return false;
      }
      return true;
    },
    changeFileError(file) {
      const index = this.uploadFileList.findIndex(item => {
        return file.uid === item.id;
      });
      if (index !== -1) {
        this.uploadFileList[index].state = FileState.Error;
      }
    },
    handleBeforeUpload(file) {
      if (this.beforeUpload) {
        // 以自定义的上传前操作优先级最高
        const flag = this.beforeUpload(file);
        if (!flag) {
          this.changeFileError(file);
        }
        return flag;
      }
      const flagArr = [];
      if (this.accept) {
        // 如果有accept,默认拿取accept作为文件格式校验
        const flag = this.checkAccept(file);
        if (!flag) {
          this.changeFileError(file);
        }
        flagArr.push(flag);
        // return flag;
      }
      if (this.fileSizeLimit) {
        const flag = this.checkFileSize(file);
        if (!flag) {
          this.changeFileError(file);
        }
        flagArr.push(flag);
      }
      return !flagArr.includes(false);
    },
    //校验文件大小
    parseSize(sizeStr) {
      const units = {
        kb: 1024,
        mb: 1024 * 1024,
        gb: 1024 * 1024 * 1024,
        tb: 1024 * 1024 * 1024 * 1024,
      };
      const regex = /(\d+)(kb|mb|gb|tb|m|g|t|k)/i;
      const matches = sizeStr.toLowerCase().match(regex);
      if (matches && matches[1] && matches[2]) {
        const value = parseInt(matches[1], 10);
        const unit = matches[2].length === 1 ? `${matches[2]}b` : matches[2];
        return value * (units[unit] || 1);
      }
      return 0;
    },
    handleInputChange(file, fileList) {
      if (this.onChange) {
        this.onChange(file, fileList);
      }
      const index = this.uploadFileList.findIndex(item => {
        return file.uid === item.id;
      });
      if (index === -1) {
        this.uploadFileList.push({
          file,
          name: file.name,
          state: FileState.Waiting,
          progress: 0,
          size: TableUtils.formatFileSize(file.size),
          speed: '速度计算中...',
          id: file.uid,
          startTime: new Date().getTime(),
        });
      }
    },
    handleProgress(event, file, fileList) {
      if (this.onProgress) {
        this.onProgress(event, file, fileList);
      }
      const index = this.uploadFileList.findIndex(item => {
        return file.uid === item.id;
      });
      if (index !== -1) {
        let timer;
        // 用于计算上传速度

        this.uploadFileList[index].state = FileState.Uploading;
        // 计算上传速度
        const currentTime = new Date().getTime();
        // const timeInterval = event.timeStamp / 1000;
        const timeInterval = (currentTime - this.uploadFileList[index].startTime) / 1000;
        const speed = event.loaded / timeInterval;
        this.uploadFileList[index].speed = `${TableUtils.formatFileSize(speed)}/秒`;
        // 计算上传进度
        const complete = Math.round((event.loaded * 100) / event.total);
        // 上传进度超过80%时,模拟进度条
        if (complete >= 70) {
          if (timer) return;
          timer = setInterval(() => {
            this.uploadFileList[index].progress += Math.round((100 - this.uploadFileList[index].progress) * 0.1);
            if (this.uploadFileList[index].progress > 99 && timer) clearInterval(timer);
          }, 1000);
        }
        this.uploadFileList[index].progress = complete;
      }
    },
  },
  watch: {
    uploadFileList: {
      handler(val) {
        this.BtnIcon = this.getBtnIcon(val);
      },
      deep: true,
    },
  },
};
</script>
<style scoped></style>

参数说明类型可选值默认值
uploadAction必选参数,上传的地址string--
uploadBtnIcon上传按钮的前置图标,一定是elementui的icon字符串stringelementui的icon‘el-icon-upload’
fileSizeLimit限制文件大小stringeq:‘150MB’、‘20KB’等,大小写不区分‘150MB’
accept接受上传的文件类型(thumbnail-mode 模式下此参数无效),并且对文件类型上传做限制string--
btnText上传按钮文字string-‘上传’

文件列表UploadCard.vue

文件列表的一些内容,包括文件名称,文件大小,上传速度,上床状态等
其中getIconByFileName方法要注意,设置自己的全局svg
<SvgIcon :icon-class="getIconByFileName(item.file)"></SvgIcon>
<template>
  <div class="ax-loading-container">
    <template v-if="uploadFileList.length > 0">
      <div class="ax-top-label">
        <i :style="'color:' + headerIconColor" :class="headerIcon"></i>
        {{ getHeaderText }}
      </div>
      <div class="ax-file-container">
        <div class="ax-file-item" v-for="(item, index) in uploadFileList" :key="index">
          <div class="ax-file-type-icon">
            <SvgIcon :icon-class="getIconByFileName(item.file)"></SvgIcon>
          </div>
          <div class="ax-file-info">
            <div class="ax-file-filename">{{ item.name }}</div>
            <div class="ax-file-loadinfo">{{ item.size }} {{ getuploadStatus(item.state) }} {{ getSpeed(item) }}</div>
          </div>
          <div class="ax-file-prograss">
            <!-- 待上传 -->
            <i v-if="item.state == 0" class="el-icon-upload2" style="color: #909399"></i>
            <!-- 上传中 -->
            <el-progress
              v-else-if="item.state == 1"
              type="circle"
              :percentage="item.progress"
              :width="30"
              :show-text="false"
              :stroke-width="3"
            ></el-progress>
            <!-- 已完成 -->
            <i v-else-if="item.state == 2" class="el-icon-circle-check" style="color: #67c23a"></i>
            <i v-else-if="item.state == 3" class="el-icon-warning" style="color: #f56c6c"></i>
          </div>
        </div>
      </div>
    </template>
    <template v-else>
      <div class="ax-top-label">暂无上传记录</div>
    </template>
  </div>
</template>

<script>
import lodash from 'lodash';
import { FileState, TableUtils } from './TableUtil';

export default {
  props: {
    uploadFileList: {
      type: Array,
      default: () => [],
    },
    hasWaitingOrUploading: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      errorCount: 0,
      waitingOrUploadingCount: 0,
    };
  },
  computed: {
    headerIcon() {
      const state = lodash(this.uploadFileList).map('state').uniq().value();

      // 优先检查是否存在错误状态 (3)
      if (state.includes(3)) {
        return 'el-icon-warning';
      }
      // 检查是否有正在上传或准备上传的文件 (0 或 1)
      if (state.includes(0) || state.includes(1)) {
        return 'el-icon-loading';
      }
      // 如果以上条件都不满足,则返回成功状态的图标
      return 'el-icon-circle-check';
    },
    headerIconColor() {
      if (this.headerIcon === 'el-icon-circle-check') {
        return '#67C23A';
      }
      if (this.headerIcon === 'el-icon-loading') {
        return '#409EFF';
      }
      if (this.headerIcon === 'el-icon-warning') {
        return '#f56c6c';
      }
      return '#409EFF';
    },

    getHeaderText() {
      if (this.waitingOrUploadingCount > 0 || this.errorCount > 0) {
        if (this.waitingOrUploadingCount > 0) {
          return `正在上传,剩余
           ${this.waitingOrUploadingCount}
         个文件,其中(有${this.errorCount}个失败)`;
        }
        return `上传任务完成,有
         ${this.errorCount}个失败`;
      }
      return '上传任务完成';
    },
  },
  mounted() {},
  methods: {
    getuploadStatus(state) {
      const mapping = ['等待上传', '上传中', '上传成功', '上传失败'];
      return mapping[state];
    },
    getSpeed(item) {
      if (item.state === 2 || item.state === 3) {
        return '';
      }
      return item.speed;
    },
    getIconByFileName(file) {
      // 文件扩展名
      const ext = file.name.split('.').pop()?.toLowerCase();

      // 文件扩展名和图标的映射关系
      const mapping = {
        audio: 'mp3,wav,aac,flac,ogg,wma,m4a',
        doc: 'doc,docx',
        pdf: 'pdf',
        ppt: 'ppt,pptx',
        txt: 'txt',
        video: 'mp4,avi,wmv,rmvb,mkv,mov,flv,f4v,m4v,rm,3gp,dat,ts,mts,vob',
        xls: 'xls,xlsx',
        zip: 'zip,rar,7z',
        pic: 'jpg,jpeg,png,gif,bmp,webp',
      };
      // 根据文件扩展名获取对应的图标
      let icon = 'file';
      Object.keys(mapping).forEach(key => {
        const exts = mapping[key].split(',');
        if (exts.includes(ext)) {
          icon = key;
        }
      });
      return `icon-${icon}-m`;
    },
    getFileStatus() {
      // 计算state等于FileState.Waiting或FileState.Uploading的元素数量
      this.waitingOrUploadingCount = this.uploadFileList.filter(
        item => item.state === FileState.Waiting || item.state === FileState.Uploading
      ).length;
      // 计算state等于FileState.Error的元素数量
      this.errorCount = this.uploadFileList.filter(item => item.state === FileState.Error).length;
    },
  },
  watch: {
    uploadFileList: {
      handler(val) {
        this.getFileStatus();
      },
      deep: true,
    },
  },
};
</script>

<style lang="scss" scoped>
.ax-loading-container {
  min-width: 300px;

  .ax-top-label {
    width: 100%;
    min-height: 40px;
    // background-color: red;
    line-height: 40px;
    font-size: 18px;
    border-bottom: 1px solid #f7f7f8;
    .el-icon-loading {
      margin-right: 10px;
    }
  }
  .ax-file-container {
    max-height: 300px;
    overflow: auto;
    width: 100%;
    .ax-file-item {
      width: 400px;
      height: 90px;
      display: flex;
      align-items: center;
      justify-content: space-evenly;

      //   background-color: red;
      .ax-file-type-icon {
        width: 60px;
        height: 60px;
        .SvgIcon {
          width: 100%;
          height: 100%;
        }
      }
      .ax-file-info {
        width: 250px;
        height: 60px;
        display: flex;
        flex-direction: column;
        justify-content: center;
        .ax-file-filename {
          width: 100%;
          overflow: hidden;
          white-space: nowrap;
          text-overflow: ellipsis;
          font-size: 16px;
          font-weight: 600;
          color: black;
          margin-bottom: 5px;
        }
        .ax-file-loadinfo {
          width: 100%;

          font-weight: 400;
          overflow: hidden;
          white-space: nowrap;
          text-overflow: ellipsis;
          color: #8e8e8e;
        }
      }
      .ax-file-prograss {
        width: 60px;
        height: 60px;

        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 20px;
      }
    }
  }
}
</style>

工具类TableUtil.js


export const FileState = {
  // 等待上传
  Waiting: 0,
  // 上传中
  Uploading: 1,
  // 上传成功
  Success: 2,
  // 上传失败
  Error: 3,
};
export class TableUtils {
  static formatFileSize(fileSize) {
    if (fileSize < 1024) {
      return `${fileSize.toFixed(2)}B`;
    }
    if (fileSize < 1024 * 1024) {
      let temp = fileSize / 1024;
      temp = +temp.toFixed(2);
      return `${temp}KB`;
    }
    if (fileSize < 1024 * 1024 * 1024) {
      let temp = fileSize / (1024 * 1024);
      temp = +temp.toFixed(2);
      return `${temp}MB`;
    }
    let temp = fileSize / (1024 * 1024 * 1024);
    temp = +temp.toFixed(2);
    return `${temp}GB`;
  }
}

  • 12
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值