Vue后台管理系统自定义导出浮窗全局组件插到body中

环境

Vue2、“axios”: “0.18.1”、webpack:“4.46.0”、ant-design-vue: “1.7.8”

背景

接到的需求大概的意思是说,因为导出数据量太大了,服务器经常504,所以导出文件使用异步处理方式,前端也要支持一下

思路:

  1. 有一个右下角浮窗,显示导出状态:处理中、处理成功、处理失败
  2. 大概2分钟轮询一次异步处理状态接口,更新导出状态
  3. 处理成功:显示“下载”按钮或者自动下载
  4. 处理中:显示“取消”按钮,可以取消此异步任务

预期效果: 弹窗必须切换标签页也能显示,随便找了个后端管理系统:JEECG BOOT演示系统

在这里插入图片描述
实现效果:

在这里插入图片描述

实现

这里建议倒着看,先看使用,然后再看如何实现

1. 定义AsyncHandlerModal组件

因为我们导入文件也会用异步处理,所以浮窗叫这个名称!用到了Ant Design Vue的图标 IconPopconfirm 气泡确认框,你也可以不用的

<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>
      &nbsp;<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(); // 开始轮询
   })
}

总结

第一次写全局组件,肯定有优化的地方,欢迎评论区指出,希望能帮助到大家!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值