首页 前端知识 vuecli AXdownload下载组件封装 css3下载悬浮球动画

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

2024-02-28 10:02:20 前端知识 前端哥 645 216 我要收藏

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

一、效果展示

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, '文件转码中...');
    },

转载请注明出处或者链接地址:https://www.qianduange.cn//article/2852.html
标签
评论
发布的文章

前端大屏适配几种方案

2024-01-29 13:01:44

JQ效果—展开和收起

2024-03-13 00:03:45

JQuery事件的基本使用

2024-03-13 00:03:39

「jQuery系列」jQuery 事件

2024-03-13 00:03:36

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!