在之前我们写过一个上传组件,现在我们在写一个下载组件,下面可能依赖了一些公用的工具类 有任何的使用疑问可以私信!!!
一、效果展示
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, '文件转码中...');
},