vuecli+AXdownload下载组件封装 +css3下载悬浮球动画效果

 更新时间:2024年05月24日 15:54:02   作者:阿里爸爸首席执行官  
当触发下载功能的时候,会触发一个下载动画,下载悬浮球会自动弹出,并且闪烁提示有新的下载任务直到下载任务完场提示,接下来通过本文介绍vuecli+AXdownload下载组件封装 +css3下载悬浮球动画效果,需要的朋友可以参考下

在之前我们写过一个上传组件,现在我们在写一个下载组件,下面可能依赖了一些公用的工具类 有任何的使用疑问可以私信!!!

一、效果展示

1. 下载触发效果

当触发下载功能的时候,会触发一个下载动画,下载悬浮球会自动弹出,并且闪烁提示有新的下载任务直到下载任务完场提示

在提示有新下载之后,悬浮球会半隐在屏幕右下角,如果所有文件的下载进度均完成,悬浮球会在1分钟后小消失

2. 下载中的进度,详情展示

点击悬浮球可以看到下载的文件列表,并对文件的下载进度,速度等信息进行展示

3.下载失败

当文件下载过程中,由于各种原因导致文件失败的时候,悬浮球会发生闪烁,并提示下载失败

二、优点

1.本组件将使用vuex对下载数据文件在session中做持久化,以防止数据在刷新页面后丢失。
2.支持多文件下载展示
3.拥有假进度条和真进度条两种,可根据业务需求,自行选择
4.对大型文件的下载给予用户动画提示,增强用户体验,以防服务器响应过长,导致页面没有响应,用户会觉得系统功能没有被触发。
5.支持私有npm包引用

三、代码展示

因为我这边已经封装到了npm包中,所以有些代码可能需要根据自己的项目进行自行调整(有任何的使用疑问可以私信!!!)
代码将分为两个部分
1. 工具类
2. ui组件部分
3. 下载方法部分

UI组件部分

工具类 TableUtil.js

export const FileState = {
  // 等待上传或者下载
  Waiting: 0,
  // 上传中或者下载中
  uploadDownloadStatus: 1,
  // 上传成功
  Success: 2,
  // 上传失败
  Error: 3,
  // 等待服务器处理
  WaitServer: 4,
};
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`;
  }
}
export function objectToFormData(obj) {
  const formData = new FormData();
  Object.keys(obj).forEach(key => {
    formData.append(key, obj[key]);
  });
  return formData;
}
export function getIconByFileName(file) {
  // 文件扩展名
  const parts = file.name.split(".");
  const ext = parts.length > 1 ? parts[parts.length - 1].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`;
}

ui组件部分 AXDownload.vue

主要是容纳悬浮球和文件列表的主容器

<template>
  <div v-if="showBall">
    <!-- 类名不要改,防止冲突 -->
    <div
      id="ax-private-download-continer"
      :class="{
        'ax-private-download-continer-add-newtask': addNewTask,
      }"
      @click="showFloatBall()"
      @mouseleave="hideFloatBall"
      @mouseenter="enterBall"
    >
      <div
        class="ax-private-download-text-content"
        :class="{
          'ax-private-circle-add-active': TaskAnminate === '添加',
          'ax-private-circle-error-active': TaskAnminate === '失败',
        }"
      >
        <div v-html="ballText"></div>
      </div>
      <DownloadFloatingBall :TaskAnminate="TaskAnminate"></DownloadFloatingBall>
    </div>
    <FileDownListDialog ref="fileDownListDialog"></FileDownListDialog>
  </div>
</template>
<script>
import DownloadFloatingBall from "./components/DownloadFloatingBall.vue";
import FileDownListDialog from "./components/FileDownListDialog.vue";
import { FileState } from "../../../src/utils/TableUtil";
export default {
  name: "AxDownLoad",
  components: {
    DownloadFloatingBall,
    FileDownListDialog,
  },
  data() {
    return {
      //显示出 悬浮球
      showDownloadBall: false,
      timer: null, //计时自动移入
      //延迟移入移出
      moveTimer: null, //移出时间器
      addNewTask: false, //是否是添加的新任务
      newTaskTimer: null,
      showBall: false,
      TaskAnminateTimer: null,
      balloldText: "我的下载",
      ballText: "",
      TaskAnminate: "",
      hideDownloadBallTimer: null,
    };
  },
  mounted() {
    const downloadList = this.$store.state.file.downloadList;
    this.showBall = downloadList.length > 0;
    this.ballText = downloadList.length > 0 ? `下载任务${"<br />"}${downloadList.length}个` : this.balloldText;
  },
  methods: {
    hideFloatBall(event) {
      this.moveTimer = setTimeout(() => {
        if (this.timer) {
          clearInterval(this.timer);
        }
        document.getElementById("ax-private-download-continer").style.transform = "translateX(0px)";
        this.showDownloadBall = false;
      }, 500);
    },
    enterBall() {
      if (this.moveTimer) {
        clearTimeout(this.moveTimer);
      }
    },
    showFloatBall() {
      if (!this.showDownloadBall) {
        //显示出 悬浮球
        this.showDownloadBall = true;
        document.getElementById("ax-private-download-continer").style.transform = "translateX(-100px)";
      } else {
        //点击悬浮球,展示下载的附件列表
        this.$refs.fileDownListDialog.showDialog({}, 0);
      }
    },
    //添加新的下载任务 动画
    addDownloadTask(text) {
      this.showDownloadBall = true;
      this.addNewTask = true;
      this.TaskAnminate = text;
      if (this.newTaskTimer) {
        clearInterval(this.newTaskTimer);
      }
      this.newTaskTimer = setTimeout(() => {
        this.addNewTask = false;
        this.TaskAnminate = "";
      }, 3000);
    },
    clearAnimateTask() {
      this.TaskAnminate = "";
      this.ballText = this.balloldText;
    },
    //延时动画
    delayAnimate(func) {
      if (this.TaskAnminateTimer) {
        clearInterval(this.TaskAnminateTimer);
      }
      this.TaskAnminateTimer = setTimeout(() => {
        func();
      }, 500);
    },
    isAllEnd(downloadList) {
      // 判断下载列表中每一个文件的状态是否为:等待、上传下载状态、等待服务器
      const flag = downloadList.every(
        item =>
          item.state !== FileState.Waiting &&
          item.state !== FileState.uploadDownloadStatus &&
          item.state !== FileState.WaitServer
      );
      if (flag) {
        if (this.hideDownloadBallTimer) {
          clearInterval(this.hideDownloadBallTimer);
        }
        //下载全部完成,隐藏悬浮球
        this.ballText = `下载任务完成`;
        this.hideDownloadBallTimer = setTimeout(() => {
          this.showBall = false;
          this.$store.commit("CLEAR_DOWNLOAD_LIST");
        }, 60000);
      } else {
        if (this.hideDownloadBallTimer) {
          clearInterval(this.hideDownloadBallTimer);
        }
      }
    },
  },
  watch: {
    showDownloadBall(newVal, oldVal) {
      if (newVal) {
        this.timer = setTimeout(() => {
          this.hideFloatBall();
        }, 5000);
      }
    },
    "$store.state.file.downloadList": {
      handler(newVal, oldVal) {
        // 在这里处理变化
        this.showBall = newVal.length > 0;
        this.balloldText = `下载任务${"<br />"}${newVal.length}个`;
        this.ballText = this.balloldText;
        this.isAllEnd(newVal);
      },
      deep: true,
    },
    "$store.state.file.errorEvent": {
      handler(newVal, oldVal) {
        this.addDownloadTask("失败");
        this.$message({
          type: "warning",
          message: `${newVal.name}下载失败了!`,
        });
        this.ballText = "下载失败!";
        this.delayAnimate(this.clearAnimateTask);
      },
      deep: true,
    },
    "$store.state.file.downloadEventCount": {
      handler(newVal, oldVal) {
        this.addDownloadTask("添加");
        this.$message({
          type: "success",
          message: "您添加了新的下载任务!",
        });
        this.ballText = "新下载!";
        this.delayAnimate(this.clearAnimateTask);
      },
      deep: true,
    },
  },
};
</script>
<style lang="scss" scoped>
#ax-private-download-continer {
  position: fixed;
  transition: transform 0.3s ease; /* 持续时间和缓动函数可以调整 */
  transform: translateX(0px); /* 初始转换状态 */
  right: -50px;
  bottom: 100px;
  width: 100px;
  height: 100px;
  z-index: 99999;
  border-radius: 100%;
  text-align: center;
  line-height: 100px;
  -webkit-user-select: none; /* Safari */
  -moz-user-select: none; /* Firefox */
  -ms-user-select: none; /* Internet Explorer/Edge */
  user-select: none; /* 非前缀版本,适用于Chrome和Opera */
  cursor: pointer;
  .ax-private-download-text-content {
    position: relative;
    color: #409eff;
    width: 90px;
    z-index: 2; /* 高于背景层 */
    line-height: 21px;
    font-weight: 600;
    top: 50%;
    right: 50%;
    transform: translate(50px, -44%);
  }
}
.ax-private-download-continer-add-newtask {
  transform: translateX(-100px) !important; /* 初始转换状态 */
}
.ax-private-circle-add-active {
  animation: addTask 1s !important;
}
.ax-private-circle-error-active {
  animation: errorTask 1s !important;
}
@keyframes addTask {
  10% {
    color: #67c23a;
  }
  80% {
    color: #c9f6b2;
  }
}
@keyframes errorTask {
  10% {
    color: white;
  }
  80% {
    color: white;
  }
}
</style>

ui组件下载悬浮球 DownloadFloatingBall.vue

下载悬浮球的主体,以及悬浮球的动画

<template>
  <!-- 类名不要改,防止冲突 -->
  <div
    class="ax-private-download-circle-container"
    :class="{
      'ax-private-download-circle-container-add-active': TaskAnminate == '添加',
      'ax-private-download-circle-container-error-active': TaskAnminate == '失败',
    }"
  >
    <div
      v-for="(item, index) in 4"
      :key="index"
      class="ax-private-circle"
      :class="{
        'ax-private-circle-active': TaskAnminate !== '',
      }"
    ></div>
  </div>
</template>
<script>
export default {
  name: "DownloadFloatingBall",
  props: {
    TaskAnminate: {
      type: String,
      default: "",
    },
  },
  data() {
    return {};
  },
};
</script>
<style scoped>
.ax-private-download-circle-container {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  width: 100px;
  height: 100px;
  border-radius: 50%;
}
.ax-private-download-circle-container-add-active {
  animation: addTaskcontainer 1s !important;
}
.ax-private-download-circle-container-error-active {
  animation: errorTaskcontainer 1s !important;
}
@keyframes addTaskcontainer {
  10% {
    background-color: #2887e6;
  }
  100% {
    background-color: transparent;
  }
}
@keyframes errorTaskcontainer {
  10% {
    background-color: #f56c6c;
  }
  100% {
    background-color: transparent;
  }
}
.ax-private-download-circle-container .ax-private-circle {
  position: absolute;
  margin: auto;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  border-radius: 50%;
  background: rgba(204, 180, 225, 0.02);
  backdrop-filter: blur(5px); /* 应用模糊效果 */
}
.ax-private-circle-active {
  animation: addTask 1.5s !important;
}
.ax-private-circle-error-active {
  animation: errorTask 1.5s !important;
}
.ax-private-download-circle-container .ax-private-circle:nth-of-type(1) {
  width: 100px;
  height: 90px;
  animation: rt 6s infinite linear;
  box-shadow: 0 0 1px 0 #2887e6, inset 0 0 10px 0 #2887e6;
}
.ax-private-download-circle-container .ax-private-circle:nth-of-type(2) {
  width: 90px;
  height: 100px;
  animation: rt 10s infinite linear;
  box-shadow: 0 0 1px 0 #006edb, inset 0 0 10px 0 #006edb;
}
.ax-private-download-circle-container .ax-private-circle:nth-of-type(3) {
  width: 105px;
  height: 95px;
  animation: rt 5s infinite linear;
  /* box-shadow: 0 0 1px 0 #003c9b, inset 0 0 10px 0 #003c9b; */
  box-shadow: 0 0 1px 0 #0148ba, inset 0 0 10px 0 #0148ba;
}
.ax-private-download-circle-container .ax-private-circle:nth-of-type(4) {
  width: 95px;
  height: 105px;
  animation: rt 15s infinite linear;
  box-shadow: 0 0 1px 0 #01acfc, inset 0 0 10px 0 #01acfc;
}
@keyframes rt {
  100% {
    transform: rotate(360deg);
  }
}
@keyframes addTask {
  10% {
    transform: scale(1.5);
  }
  30% {
    transform: scale(0.6);
  }
  60% {
    transform: scale(1);
  }
}
</style>

ui组件下载文件列表弹窗 FileDownListDialog

主要是点击悬浮球之后的弹窗,用于展示文件的列表

<template>
  <!-- 对话框 -->
  <el-dialog
    v-if="dialog.visible"
    ref="dialog"
    :title="getHeaderText"
    :visible.sync="dialog.visible"
    width="70%"
    :close-on-click-modal="false"
  >
    <div class="ax-private-file-container">
      <template v-if="fileTaskList.length > 0">
        <div class="ax-private-file-item" v-for="(item, index) in fileTaskList" :key="index">
          <div class="ax-file-progress" :style="{ width: `${item.process}%` }"></div>
          <div class="ax-file-content">
            <div class="ax-file-type-icon">
              <SvgIcon :icon-class="getIconByFileName({ name: item.name })"></SvgIcon>
            </div>
            <div class="ax-file-info">
              <div class="ax-file-filename">{{ item.name }}</div>
              <div class="ax-file-loadinfo">
                <span class="info-span">已下载:{{ item.loaded }}</span>
                <span class="info-span" v-if="item.size !== 'NaNGB'">文件大小:{{ item.size }}</span>
                {{ getuploadStatus(item.state, item.message) }}
                <span
                  style="color: #409eff; cursor: pointer"
                  v-if="item.message && item.state == 3"
                  @click="showError(item.message)"
                >
                  查看详情</span
                >
                {{ getSpeed(item) }}
              </div>
            </div>
            <div class="ax-file-operate">
              <i v-if="item.state == 0" class="el-icon-download" style="color: #909399"></i>
              <!-- 上传中 -->
              <span v-else-if="item.state == 1 || item.state == 4"> {{ item.process }}%</span>
              <!-- 已完成 -->
              <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>
    <el-row type="flex" justify="end"> </el-row>
  </el-dialog>
</template>
<script>
import { getIconByFileName, FileState } from "../../../../src/utils/TableUtil.js";
const STATUS = {
  CREATE: 0,
  UPDATE: 1,
};
export default {
  name: "FileDownListDialog",
  props: {
    // 对话框标题
    textMap: {
      type: Object,
      default: () => ({
        add: "文件下载列表",
        edit: "编辑",
      }),
    },
  },
  data() {
    return {
      fileTaskList: [],
      // 对话框
      dialog: {
        // 对话框状态
        status: null,
        // 对话框参数,用于编辑时暂存id
        params: {},
        // 对话框是否显示
        visible: false,
      },
      errorCount: 0,
      waitingOrUploadingCount: 0,
    };
  },
  computed: {
    // 对话框标题
    dialogTitle() {
      return this.dialog.status === STATUS.CREATE ? this.textMap.add : this.textMap.edit;
    },
    getHeaderText() {
      if (this.waitingOrUploadingCount > 0 || this.errorCount > 0) {
        if (this.waitingOrUploadingCount > 0) {
          return `正在下载,剩余
           ${this.waitingOrUploadingCount}
         个文件,其中(有${this.errorCount}个失败)`;
        }
        return `下载任务完成,有
         ${this.errorCount}个失败`;
      }
      return "所有下载任务完成";
    },
  },
  methods: {
    /**
     * 显示对话框,父元素调用
     *
     * @param {Object} param 对话框保存时的参数
     * @param {Number} status 对话框状态[添加:0,编辑:1],必须是STATUS枚举
     * @param {Object} formValues 编辑时传入所有字段的默认值
     */
    async showDialog(param = {}, status = STATUS.CREATE) {
      // 保存参数用于save方法
      this.dialog.params = param;
      this.dialog.status = status;
      this.fileTaskList = this.$store.state.file.downloadList;
      this.getFileStatus();
      this.dialog.visible = true;
    },
    getIconByFileName(item) {
      const file = {
        name: item.name,
      };
      return getIconByFileName(file);
    },
    // 取消按钮点击
    btnCancelOnClick() {
      this.dialog.visible = false;
      this.$emit("cancel");
    },
    showError(message) {
      this.$message.error(message);
    },
    getuploadStatus(state, message) {
      const mapping = ["等待下载,请稍后...", "下载中", "下载成功", "下载失败", "等待服务器处理"];
      if (message) {
        return message.slice(0, 15);
      }
      return mapping[state];
    },
    getSpeed(item) {
      if (item.state === 2 || item.state === 3 || item.state === 4) {
        return "";
      }
      return item.state === 1 && item.speed === "速度计算中..." ? "" : item.speed;
    },
    getFileStatus() {
      // 计算state等于FileState.Waiting或FileState.Uploading的元素数量
      this.waitingOrUploadingCount = this.fileTaskList.filter(
        item =>
          item.state === FileState.WaitServer ||
          item.state === FileState.Waiting ||
          item.state === FileState.uploadDownloadStatus
      ).length;
      // 计算state等于FileState.Error的元素数量
      this.errorCount = this.fileTaskList.filter(item => item.state === FileState.Error).length;
    },
  },
  watch: {
    "$store.state.file.downloadList": {
      handler(newVal, oldVal) {
        // 在这里处理变化
        this.fileTaskList = newVal;
        this.getFileStatus();
      },
      deep: true,
    },
  },
};
</script>
<style lang="scss" scoped>
::v-deep .el-dialog__body {
  height: 680px;
}
.ax-private-file-container {
  width: 100%;
  height: 600px;
  overflow: auto;
  .ax-private-file-item {
    float: left;
    width: 100%;
    height: 100px;
    position: relative;
    .ax-file-progress {
      height: 100px;
      background-color: #f5f9ff;
      position: absolute;
      z-index: 0;
      left: 0px;
    }
    .ax-file-content {
      z-index: 9999;
      width: 100%;
      position: absolute;
      height: 100px;
      display: flex;
      align-items: center;
      border-bottom: 1px solid #e0e2e6;
    }
    .ax-file-type-icon {
      width: 70px;
      height: 70px;
      float: left;
      .SvgIcon {
        width: 100%;
        height: 100%;
      }
    }
    .ax-file-info {
      width: calc(100% - 170px);
      float: left;
      //   background-color: red;
      .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;
        .info-span {
          margin-right: 10px;
        }
      }
    }
    .ax-file-operate {
      width: 100px;
      height: 100px;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 20px;
      float: right;
    }
  }
}
</style>

下载工具方法 download.ts

主要触发ui动画,触发下载的方法

import Vue from "vue";
import { MessageBox } from "element-ui"; // eslint-disable-line
import guid from "./generator";
import { FileState, TableUtils } from "./TableUtil.js";
// import store from "../store/index";
interface FileItem {
  name: string;
  state?: number;
  size: number | string; //文件大小转义 类似10mb
  total?: number | string; //文件字节大小 114882037
  loaded?: number | string; //已下载大小
  process?: number;
  speed?: string;
  id: string; //唯一键
  realId?: string; //真实文件id
  startTime?: number;
  message?: string; //文件下载提示一些文字或者错误
}
interface FilePojo {
  name: string; //文件名称
  id?: string; //文件id
  size?: string | number; //文件大小
  total?: string | number; //文件总大小
}
function getRandomInt(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1)) + min;
}
//模拟随机进度
function getRandomProcess(fileItem) {
  let percentCompleted = 0;
  const randomInt = getRandomInt(1, 2);
  const randomMaxPro = getRandomInt(94, 97);
  if (fileItem.process < randomMaxPro) {
    fileItem.process += randomInt;
    percentCompleted = fileItem.process;
  } else {
    //无操作
    percentCompleted = fileItem.process;
  }
  return percentCompleted;
}
//判断total是否为未知
function isHasTotal(fileItem, loaded, total) {
  let percentCompleted = 0;
  //如果total为0
  if (total === 0) {
    //如果文件大小为0,就说明文件的大小属于未知状态,需要模拟进度条
    percentCompleted = getRandomProcess(fileItem);
  } else {
    //如果文件大小不为0,就可以计算真实的下载进度
    const realProcess = Math.round((loaded * 100) / total);
    if (realProcess > 80) {
      percentCompleted = getRandomProcess(fileItem);
    } else {
      percentCompleted = realProcess;
    }
  }
  return percentCompleted;
}
//监听下载进度
function onDownloadProgress(progressEvent, file) {
  //获取下载列表
  const downloadList = Vue.prototype.$store.getters.downloadList;
  //如果下载列表不为空,且下载列表长度大于0
  if (downloadList && downloadList.length > 0) {
    //在下载列表中查找id与文件id相同的文件
    const index = downloadList.findIndex(i => i.id === file.id);
    let percentCompleted = 0;
    percentCompleted = isHasTotal(
      downloadList[index],
      progressEvent.loaded,
      file.total === 0 ? progressEvent.total : file.total
    );
    //如果索引大于-1,说明文件在下载列表中
    if (index > -1) {
      const currentTime = new Date().getTime();
      const timeInterval = (currentTime - downloadList[index].startTime) / 1000;
      const speed = progressEvent.loaded / timeInterval;
      downloadList[index].speed = `${TableUtils.formatFileSize(speed)}/秒`;
      const randomMaxPro = getRandomInt(94, 97);
      //更新进度条
      downloadList[index].process = percentCompleted;
      downloadList[index].loaded = TableUtils.formatFileSize(progressEvent.loaded);
      //更新文件状态
      downloadList[index].state = FileState.uploadDownloadStatus;
      if (percentCompleted >= randomMaxPro) {
        //说明已经进入了模拟进度
        downloadList[index].state = FileState.WaitServer;
      }
      const fileItem = downloadList[index];
      Vue.prototype.$store.commit("UPDATE_DOWNLOAD_ITEM", { item: fileItem, index: index });
    }
  }
}
//获取下载文件存进session
function setFileSessionStorage(file) {
  const newFile: FileItem = {
    name: file.name,
    state: FileState.Waiting,
    size: file.size || "未知",
    total: file.total || "未知",
    loaded: 0 || "未知", //已下载大小
    process: 0,
    speed: "速度计算中...",
    id: file.id,
    realId: file.realId,
    message: file.message || "",
    startTime: new Date().getTime(),
  };
  //判断是否已经存在
  const downloadList = Vue.prototype.$store.getters.downloadList;
  // 如果下载列表存在且长度大于0
  if (downloadList && downloadList.length > 0) {
    // 查找下载列表中是否有与文件id相同的文件
    const index = downloadList.findIndex(i => i.id === file.id);
    // 如果没有找到
    if (index === -1) {
      // 将文件添加到下载列表中
      Vue.prototype.$store.commit("ADD_DOWNLOAD_ITEM", newFile);
    } else {
      // 如果找到,更新下载列表中的文件
      Vue.prototype.$store.commit("UPDATE_DOWNLOAD_ITEM", { item: newFile, index: index });
    }
  } else {
    // 如果下载列表不存在或长度等于0,将文件添加到下载列表中
    Vue.prototype.$store.commit("SET_DOWNLOAD_LIST", [newFile]);
  }
  Vue.prototype.$store.commit("ADD_DOWNLOAD_EVENT_COUNT");
}
//判断是get还是post
function isMethod(file, url, method, data, params) {
  return Vue.prototype.axios({
    url: url,
    method: method,
    responseType: "blob", // 确保以blob形式接收文件数据
    data: data,
    params: params, // 将查询参数添加到请求中
    onDownloadProgress: progressEvent => {
      onDownloadProgress(progressEvent, file);
    },
  });
}
function setFileName(name) {
  const date = new Date();
  let fileName;
  if (/^.*\..{1,4}$/.test(name)) {
    fileName = name;
  } else {
    fileName = `${name} ${date.getFullYear()}年${date.getMonth() + 1}月
      ${date.getDate()}日${date.getHours()}时${date.getMinutes()}分${date.getSeconds()}秒.xls`;
  }
  return fileName;
}
/**
 * 通用下载 老版本
 *
 * @export
 * @param {String} url 请求地址
 * @param {String} name 文件名
 * @param {Object} params 请求参数
 * @param {String} requestType 请求方式(get,post)
 * @param {function} callBackFun 回调函数
 */
// eslint-disable-next-line
export  function download(url, name, data, requestType = 'get', params, callBackFun: Function = () => { },file?:FilePojo) {
  let axiosObj;
  const fileName = setFileName(name);
  let fileObj: FileItem = {
    name: fileName,
    id: guid(),
    size: "未知",
    realId: "",
    total: 0,
  };
  if (file) {
    fileObj = {
      name: file.name || fileName,
      id: guid(),
      realId: file.id || "",
      size: TableUtils.formatFileSize(Number(file.size)) || "未知",
      total: Number(file.size) || 0,
    };
  }
  //将即将要下载的文件存进session中
  setFileSessionStorage(fileObj);
  if (requestType === "get") {
    axiosObj = isMethod(fileObj, url, "get", {}, params);
  } else {
    //    axios.post(url, data, { responseType: "blob", params });
    axiosObj = isMethod(fileObj, url, "post", data, params);
  }
  axiosObj
    .then(res => {
      //获取下载列表
      const downloadList = Vue.prototype.$store.getters.downloadList;
      const index = downloadList.findIndex(i => i.id === fileObj.id);
      if (!res) {
        //返回数据异常,附件要求失败
        if (index !== -1) {
          //更新文件状态
          downloadList[index].state = FileState.Error;
          downloadList[index].message = res.message || res.data.message || "文件下载失败";
          const fileItem = downloadList[index];
          Vue.prototype.$store.commit("UPDATE_DOWNLOAD_ITEM", { item: fileItem, index: index });
          Vue.prototype.$store.commit("ERROR_EVENT", fileItem.name);
        }
        return;
      }
      // 如果返回类型为json 代表导出失败 此时读取后端返回报错信息
      if (res.type === "application/json") {
        const reader: any = new FileReader(); // 创建一个FileReader实例
        reader.readAsText(res, "utf-8"); // 读取文件,结果用字符串形式表示
        reader.onload = () => {
          // 读取完成后,**获取reader.result**
          const { message } = JSON.parse(reader.result);
          downloadList[index].state = FileState.Error;
          downloadList[index].message = message || "文件下载失败";
          const fileItem = downloadList[index];
          Vue.prototype.$store.commit("UPDATE_DOWNLOAD_ITEM", { item: fileItem, index: index });
          Vue.prototype.$store.commit("ERROR_EVENT", fileItem.name);
          // 请求出错
          MessageBox.alert(`${message}`, "操作失败", {
            confirmButtonText: "我知道了",
            type: "warning",
            showClose: true,
          });
        };
        if (callBackFun) callBackFun("error");
        return;
      }
      const blob = new Blob([res]);
      let fileName;
      const date = new Date();
      if (/^.*\..{1,4}$/.test(name)) {
        fileName = name;
      } else if (res.headers && res.headers.includes("fileName=")) {
        fileName = decodeURIComponent(res.headers.split("fileName=")[1]);
      } else if (res.headers && res.headers.includes(`fileName*=utf-8''`)) {
        fileName = decodeURIComponent(res.headers.split(`fileName*=utf-8''`)[1]);
      } else {
        fileName = `${name} ${date.getFullYear()}年${
          date.getMonth() + 1
        }月${date.getDate()}日${date.getHours()}时${date.getMinutes()}分${date.getSeconds()}秒.xls`;
      }
      downloadList[index].name = fileName;
      downloadList[index].state = FileState.Success;
      downloadList[index].process = 100;
      const fileItem = downloadList[index];
      Vue.prototype.$store.commit("UPDATE_DOWNLOAD_ITEM", { item: fileItem, index: index });
      const aTag = document.createElement("a");
      aTag.style.display = "none";
      aTag.download = fileName;
      aTag.href = URL.createObjectURL(blob);
      document.body.appendChild(aTag);
      aTag.click();
      URL.revokeObjectURL(aTag.href);
      document.body.removeChild(aTag);
      if (callBackFun) callBackFun();
    })
    .catch(error => {
      // 处理错误
      const downloadList = Vue.prototype.$store.getters.downloadList;
      const index = downloadList.findIndex(i => i.id === fileObj.id);
      if (index !== -1) {
        //更新文件状态
        downloadList[index].state = FileState.Error;
        const msg = JSON.stringify(error);
        downloadList[index].message = error.message || `文件下载失败!${msg}`;
        const fileItem = downloadList[index];
        Vue.prototype.$store.commit("UPDATE_DOWNLOAD_ITEM", { item: fileItem, index: index });
        Vue.prototype.$store.commit("ERROR_EVENT", fileItem.name);
      }
    });
}
//新版本 推荐
export function downloadFile({ url, name, data, method, params, callBackFun, file }) {
  download(url, name, data, method, params, callBackFun, file);
}
//不走接口,虚假进度条
export function fakeDownProgress(file: FilePojo, func, funcArgs, message) {
  if (!file) {
    console.error("文件类型异常,file不能为null");
    return;
  }
  const fileObj = {
    name: file.name,
    id: guid(),
    realId: file.id || "",
    size: TableUtils.formatFileSize(Number(file.size)) || "未知",
    total: Number(file.size) || 0,
    message: message || "任务进行中",
  };
  setFileSessionStorage(fileObj);
  let timer;
  const downloadList = Vue.prototype.$store.getters.downloadList;
  const index = downloadList.findIndex(i => i.id === fileObj.id);
  if (index !== -1) {
    if (timer) {
      clearInterval(timer);
    }
    timer = setInterval(() => {
      downloadList[index].state = FileState.uploadDownloadStatus;
      const percentCompleted = isHasTotal(downloadList[index], 0, 0);
      downloadList[index].process = percentCompleted;
      const fileItem = downloadList[index];
      Vue.prototype.$store.commit("UPDATE_DOWNLOAD_ITEM", { item: fileItem, index: index });
    }, getRandomInt(800, 2000));
  }
  // eslint-disable-next-line no-async-promise-executor
  new Promise(async (resolve, reject) => {
    const res = await func(funcArgs);
    console.log(res);
    resolve(res);
  }).then(state => {
    console.log("state", state);
    if (timer) {
      clearInterval(timer);
    }
    console.log(index);
    if (index !== -1) {
      downloadList[index].state = state;
      if (downloadList[index].state === FileState.Success) {
        downloadList[index].process = 100;
        downloadList[index].message = "";
        const fileItem = downloadList[index];
        Vue.prototype.$store.commit("UPDATE_DOWNLOAD_ITEM", { item: fileItem, index: index });
      }
      if (downloadList[index].state === FileState.Error) {
        const fileItem = downloadList[index];
        Vue.prototype.$store.commit("UPDATE_DOWNLOAD_ITEM", { item: fileItem, index: index });
        Vue.prototype.$store.commit("ERROR_EVENT", fileItem.name);
      }
    }
  });
}

当我们注意到再download的方法中多次使用了store,所以我们要使用到vuex来做持久化

对应的store对象

const file = {
  state: {
    downloadList: [], //文件下载列表
    downloadEventCount: 0, //文件下载触发次数
    errorEvent: {
      count: 0,
      name: "",
    }, //错误事件触发
    successEvent: 0, //成功事件触发
  },
  mutations: {
    SET_DOWNLOAD_LIST: (state, list) => {
      state.downloadList = list;
    },
    ADD_DOWNLOAD_EVENT_COUNT: state => {
      state.downloadEventCount += 1;
    },
    ADD_DOWNLOAD_ITEM: (state, item) => {
      state.downloadList = [...state.downloadList, item];
    },
    //修改downloadList其中的某个元素
    UPDATE_DOWNLOAD_ITEM: (state, { item, index }) => {
      state.downloadList.splice(index, 1, item);
    },
    //删除downloadList所有元素
    CLEAR_DOWNLOAD_LIST: state => {
      state.downloadList = [];
    },
    CLEAR_ERROR_EVENT: state => {
      state.errorEvent.count = 0;
      state.errorEvent.name = "";
    },
    ERROR_EVENT: (state, name) => {
      state.errorEvent.count += 1;
      state.errorEvent.name = name;
    },
    SUCCESS_EVENT: state => {
      state.successEvent += 1;
    },
  },
  actions: {},
};
export default file;

持久化vuex store对象的入口处

import Vue from "vue";
import Vuex from "vuex";
import createPersistedState from "vuex-persistedstate";
import app from "./modules/app";
import user from "./modules/user";
import file from "./modules/file";
import getters from "./getters";
Vue.use(Vuex);
const store = new Vuex.Store({
  // 注意:新增的modules如果需要持久化还需要在plugins配置一下
  modules: {
    app,
    user,
    file,
  },
  getters,
  // 局部持久化,之所以不能全部持久化,详见src/permission.js
  plugins: [
    createPersistedState({
      paths: ["app", "file"],
      storage: window.sessionStorage,
    }),
  ],
});
export default store;

getters中配置对应的属性,用于获取

const getters = {
  //文件管理
  downloadList: state => state.file.downloadList,
};
export default getters;

下载组件的使用

在使用download.ts中的方法触发下载之前,需要引入ui组件,在App.vue中,引用

<template>
  <div id="app" v-loading.fullscreen.lock="$store.state.app.isLoading" element-loading-text="请稍候">
    <router-view />
    <AxDownLoad></AxDownLoad>
  </div>
</template>

在使用下载组件的时候会用到的一些内部方法

      import {download,downloadFile, fakeDownProgress, FileState } from 'download.ts';

使用例子 采用下列方法,可以明确传递的参数是什么,便于后续维护更新,复用

 btnDownloadOnClick(row) {
      const { fileName, fileExtension, pictureBase64Code, affixId } = row;
        const url = `${this.API_URL}iqpQuery/file/flowAffixDownload`;
        const params = { affixInfoId: affixId };
        //采用下列方法,可以明确传递的参数是什么,便于后续维护更新,复用
        downloadFile({
          url,
          name: `${fileName}.${fileExtension}`,
          params,
        });
    },

如果希望下载进度为真实进度,那么可以考虑上传file这个对象,里面的size,把真实的文件大小传入,或者由服务端在header加上contentLength

fakeDownProgress方法

此方法为虚假的进度展示,以便于一些没有进度功能的长期方法的进度展示,
//使用fakeDownProgress方法进行进度展示,
//依次参数说明
//file:为FilePojo类型,可以传递文件id,也可以不传递,name必须传递
//func:需要等待的方法
//funcArgs:方法需要传递的对象,
//message:进度展示的文字信息
使用例子
//这是一个文件转码的方法,消耗时间的大小,不可计算,需要使用Promise方法进行包裹,除此以外,可以再执行完成后的使用 resolve(FileState.Success);,失败同理!

// A code block
var foo = 'bar';
 Base64FileEvent({ id, base64String, fileName, fileExtension }) {
      return new Promise((resolve, reject) => {
        const byteCharacters = atob(base64String);
        const byteNumbers = new Array(byteCharacters.length);
        // eslint-disable-next-line no-plusplus
        for (let i = 0; i < byteCharacters.length; i++) {
          byteNumbers[i] = byteCharacters.charCodeAt(i);
        }
        const byteArray = new Uint8Array(byteNumbers);
        const blob = new Blob([byteArray], { type: 'application/octet-stream' });
        const downloadLink = document.createElement('a');
        const url = window.URL.createObjectURL(blob);
        console.log(url);
        downloadLink.href = url;
        downloadLink.download = `${fileName}.${fileExtension}`;
          downloadLink.click();
EventListener('click', () => {
          document.body.removeChild(downloadLink);
          window.URL.revokeObjectURL(url);
          resolve(FileState.Success);
        });
        // setTimeout(() => {
        //   resolve(2);
        // }, 2000);
      });
    },
     downloadBase64AsFile(id, base64String, fileName, fileExtension) {
      const data = {
        id,
        base64String,
        fileName,
        fileExtension,
      };
      const file = {
        id,
        name: `${fileName}.${fileExtension}`,
      };
      //使用fakeDownProgress方法进行进度展示,
      //依次参数说明
      //file:为FilePojo类型,可以传递文件id,也可以不传递,name必须传递
      //func:需要等待的方法
      //funcArgs:方法需要传递的对象,
      //message:进度展示的文字信息
      fakeDownProgress(file, this.Base64FileEvent, data, '文件转码中...');
    },

到此这篇关于vuecli+AXdownload下载组件封装 +css3下载悬浮球动画的文章就介绍到这了,更多相关vuecli+AXdownload下载组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解在vue-test-utils中mock全局对象

    详解在vue-test-utils中mock全局对象

    这篇文章主要介绍了详解在vue-test-utils中mock全局对象,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-11-11
  • vue3渲染函数(h函数)的变更剖析

    vue3渲染函数(h函数)的变更剖析

    这篇文章主要介绍了vue3渲染函数(h函数)的变化,文中给大家介绍了h函数的三个参数详细说明及vue3 h函数-绑定事件,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-10-10
  • vue 中Virtual Dom被创建的方法

    vue 中Virtual Dom被创建的方法

    本文将通过解读render函数的源码,来分析vue中的vNode是如何创建的,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-04-04
  • vue获取input值的三种常用写法

    vue获取input值的三种常用写法

    这篇文章主要介绍了vue获取input值的三种常用写法,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-08-08
  • Vue.use()在new Vue() 之前使用的原因浅析

    Vue.use()在new Vue() 之前使用的原因浅析

    本文通过实例代码给大家介绍了为什么Vue.use()在new Vue() 之前使用,需要的朋友可以参考下
    2019-08-08
  • vue实现公告栏文字上下滚动效果的示例代码

    vue实现公告栏文字上下滚动效果的示例代码

    这篇文章主要介绍了vue实现公告栏文字上下滚动效果的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-06-06
  • VueRouter导航守卫用法详解

    VueRouter导航守卫用法详解

    这篇文章主要介绍了VueRouter导航守卫的详细用法以及分类介绍,有兴趣的朋友学习参考下吧。
    2017-12-12
  • vue 实现在函数中触发路由跳转的示例

    vue 实现在函数中触发路由跳转的示例

    今天小编就为大家分享一篇vue 实现在函数中触发路由跳转的示例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-09-09
  • vue-element-admin如何设置默认语言

    vue-element-admin如何设置默认语言

    这篇文章主要介绍了vue-element-admin如何设置默认语言,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-04-04
  • vue中router.beforeEach()的简单用法举例

    vue中router.beforeEach()的简单用法举例

    router.beforeEach()一般用来做一些进入页面的限制,比如没有登录,就不能进入某些页面,只有登录了之后才有权限查看某些页面,下面这篇文章主要给大家介绍了关于vue中router.beforeEach()的简单用法举例,需要的朋友可以参考下
    2023-01-01

最新评论