环境
Vue2、“axios”: “0.18.1”、webpack:“4.46.0”、ant-design-vue: “1.7.8”
背景
接到的需求大概的意思是说,因为导出数据量太大了,服务器经常504,所以导出文件使用异步处理方式,前端也要支持一下
思路:
- 有一个右下角浮窗,显示导出状态:处理中、处理成功、处理失败
- 大概2分钟轮询一次异步处理状态接口,更新导出状态
- 处理成功:显示“下载”按钮或者自动下载
- 处理中:显示“取消”按钮,可以取消此异步任务
预期效果: 弹窗必须切换标签页也能显示,随便找了个后端管理系统:JEECG BOOT演示系统
实现效果:
实现
这里建议倒着看,先看使用,然后再看如何实现
1. 定义AsyncHandlerModal组件
因为我们导入文件也会用异步处理,所以浮窗叫这个名称!用到了Ant Design Vue的图标 Icon、Popconfirm 气泡确认框,你也可以不用的
<template>
<div class="bc-async-handler-modal">
<div class="progress-header">
<template v-if="taskStatus == 2">
<a-icon type="check-circle" theme="twoTone" :two-tone-color="themeColor" />
</template>
<template v-else>
<a-icon type="clock-circle" theme="twoTone" :two-tone-color="themeColor" />
</template>
<span :style="{ color: themeColor }">处理提示</span>
</div>
<div class="progress-content" :style="{ color: taskStatus == 3 ? 'red' : undefined }">
文件《<span>{{ options.fileName }}》</span>{{ statusText }}
</div>
<div class="progress-footer">
<template v-if="fileUrl">
<button class="download-btn" @click="handleDownload(fileUrl)">下载</button>
</template>
<template v-else>
<a-popconfirm title="确定取消处理此异步任务吗?" @confirm="handleCancel">
<button class="close-btn">取消</button>
</a-popconfirm>
</template>
</div>
</div>
</template>
<script>
import { baseUrl } from '@/constants'
import { getAction } from '@/api/manage'
export default {
name: "AsyncHandlerModal",
data() {
return {
options: {
fileName: "文件内容",
pollingUrl: "轮询URL",
pollingParams: null,
overUrl: "结束异步工作的URL",
},
statusText: "正在处理中...",
taskStatus: 1,
fileUrl: "",
themeColor: "",
timer: null,
}
},
watch: {
taskStatus: {
handler(value) {
switch (value) {
case 0:
this.themeColor = "#A3A3A3"; // 灰色
break;
case 1:
this.themeColor = "#1890ff"; // 蓝色
break;
case 2:
this.themeColor = "#52c41a"; // 绿色
break;
case 3:
this.themeColor = "#ff4d4f"; // 红色
break;
}
},
immediate: true,
}
},
mounted() {
},
methods: {
start() {
this.goToPolling();
},
close() {
this.clearTimer();
},
destroy() {
this.clearTimer();
this.detachFromBody();
},
attachToBody(element) {
document.body.appendChild(element);
},
detachFromBody() {
const element = document.querySelector('.bc-async-handler-modal');
if (element) {
element.parentNode.removeChild(element);
}
},
// 轮询
goToPolling() {
this.clearTimer(); // 清楚定时器
this.timer = setInterval(() => {
this.getTaskStatus();
}, 10 * 1000);
},
getTaskStatus() {
const { pollingUrl, pollingParams } = this.options;
if (!pollingUrl) return;
getAction(pollingUrl, pollingParams).then(res => {
if (res.success) {
const taskStatus = res.result.taskStatus; // 任务状态: 0:待处理 1:处理中 2:处理成功 3:处理失败
if (taskStatus == 1) {
this.statusText = "正在处理中..."
} else if (taskStatus == 2) {
this.statusText = "处理完成,请点击蓝色按钮下载文件!"
this.fileUrl = res.result.fileUrl;
} else if (taskStatus == 3) {
this.statusText = handleResult
}
if (taskStatus == 2 || taskStatus == 3) {
clearInterval(this.timer); // 处理有结果了,不需要轮询了
}
this.taskStatus = taskStatus;
} else {
this.$message.error(res.message);
}
})
},
handleCancel() {},
handleDownload() {
// 创建一个隐藏的a标签
const link = document.createElement('a');
link.style.display = 'none'; // 隐藏a标签
link.href = `${baseUrl}${this.fileUrl}`;
// 将a标签添加到body中
document.body.appendChild(link);
// 模拟点击a标签
link.click();
// 然后移除a标签
document.body.removeChild(link);
//下载完成,销毁弹窗
this.destroy();
},
clearTimer() {
if (this.timer) {
clearInterval(this.timer); // 清除定时器
this.timer = null;
}
},
}
}
</script>
<style lang="less">
.bc-async-handler-modal {
position: fixed;
right: 10px;
bottom: 25px;
max-width: 260px;
min-width: 220px;
padding: 13px 17px;
background-color: rgba(255, 255, 255);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
border-radius: 5px;
z-index: 9999; // 确保浮窗显示在其他内容之上
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
transition: opacity 0.3s ease-in-out; // 淡入淡出效果
&.hidden {
opacity: 0;
pointer-events: none;
}
.progress-header {
width: 100%;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
}
.progress-content {
width: 100%;
margin-top: 10px;
margin-bottom: 15px;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
}
.progress-footer {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
}
.download-btn {
font-size: 13px;
padding: 3px 7px;
border: none;
background-color: #007bff; // 蓝色背景
color: #fff;
border-radius: 3px;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
&:hover {
background-color: #0056b3; // 蓝色变深
}
}
.close-btn {
font-size: 13px;
padding: 3px 7px;
border: none;
background-color: #ccc;
color: #fff;
border-radius: 3px;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
&:hover {
background-color: #aaa;
}
}
}
</style>
2. 定义createModal方法
前面只是声明了组件,那怎么调用?
需要在文档之外渲染并且挂载到body节点,并且能支持传参数,像父子组件通过props传参一样,如此做到呢?
需要用到 Vue.extend
参考博客:vue 自定义组件插入到body中的实现
代码如下:
import Vue from 'vue'
import AsyncHandlerModal from './AsyncHandlerModal.vue'
export default function createAsyncHandlerModal(options) {
const BCAsyncHandlerModal = Vue.extend(AsyncHandlerModal)
const vnode = new BCAsyncHandlerModal().$mount()
vnode.options = options;
vnode.show = () => {
const oldDom = document.querySelector('.bc-async-handler-modal');
if (!oldDom) vnode.attachToBody(vnode.$el); // 保证唯一性,一个系统一个异步处理弹窗
};
return vnode;
}
3. 使用弹窗
点击了“导出”按钮,执行下面 handleExportConfirm 方法:
import createAsyncHandlerModal from "@/components/BCModal/AsyncHandlerModal.js"
handleExportConfirm() {
// 请求接口,开始异步导出
getAction('/user/asyncExport', params).then(res => {
const asyncHandlerModal = createAsyncHandlerModal({
fileName: "用户数据",
pollingUrl: '/user/getExportStatus'
});
asyncHandlerModal.show(); // 显示弹窗
asyncHandlerModal.start(); // 开始轮询
})
}
总结
第一次写全局组件,肯定有优化的地方,欢迎评论区指出,希望能帮助到大家!