分块下载时显示下载进度(Vue + SpringBoot)

描述:后端变全栈,记录下牛马生活的一些功能实现。实现效果如下:

前端Vue实现#

<template>
  <el-table :data="tableData" style="width: 100%">
    <el-table-column prop="date" label="日期" width="180"></el-table-column>
    <el-table-column prop="name" label="姓名" width="180"></el-table-column>
    <el-table-column prop="address" label="地址"></el-table-column>
    <el-table-column label="操作" width="120">
      <template slot-scope="scope">
        <div style="display: flex; align-items: center; height: 100%;">
          <el-button @click="downLoad(scope.row)" type="text" size="small">下载</el-button>
          <div :id="scope.row.id" style="width: 35%; height: 35%; position: relative; margin-left: 30px;">
            <div v-if="isShowChart[scope.row.id]" ></div>
          </div>
        </div>
      </template>
    </el-table-column>
  </el-table>
</template>

<script>
import config from '@/constant/config'
import * as echarts from 'echarts'
export default {
  data () {
    return {
      current: 0, // 当前进度
      total: 1, // 总进度
      chartArr: [], // 进度条图表数组
      isShowChart: {}, // 是否显示进度条图表
      downLoadArr: [], // 下载数组
      historyNum: 0, // 历史进度
      tableData: [
        { id: '1', date: '2023-10-01', name: '张三', address: '北京市', filePath: '/data/app/images/20250122_141546.zip' }
      ]
    }
  },
  methods: {
    // 进度条初始化方法
    initBarChart (id, progress, color) {
      const percentage = Math.round(progress * 100) // 将进度转换为百分比
      const option = {
        title: [{
          text: `${percentage}%`, // 显示进度
          x: '38%',
          y: '26%',
          textAlign: 'center',
          textStyle: {
            fontSize: '10',
            fontWeight: '100',
            textAlign: 'center'
          }
        }],
        polar: {
          radius: ['90%', '80%'], // 增大圆形
          center: ['50%', '50%']
        },
        angleAxis: {
          max: 100,
          show: false
        },
        radiusAxis: {
          type: 'category',
          show: true,
          axisLabel: { show: false },
          axisLine: { show: false },
          axisTick: { show: false }
        },
        series: [{
          name: '',
          type: 'bar',
          roundCap: true,
          barWidth: 70,
          showBackground: true,
          backgroundStyle: { color: '#f3f3f3' },
          data: [percentage], // 设置进度数据
          coordinateSystem: 'polar',
          itemStyle: {
            normal: { color } // 设置进度条颜色
          }
        }]
      }
      const pieChart = echarts.init(document.getElementById(id)) // 初始化图表
      pieChart.setOption(option) // 设置图表选项
      this.chartArr.push(pieChart) // 将图表添加到数组中

      // 如果进度达到100%,则隐藏图表并重新初始化页面数据
      if (percentage >= 100) {
        this.$set(this.isShowChart, id, false)
        this.downLoadArr = [] // 清空下载数组
        this.historyNum = 0
        this.current = 0
        this.total = 1
        this.$forceUpdate() // 强制更新视图
      }
    },
    // 下载数据的方法
    downLoad (data) {
      // 显示图表
      this.$set(this.isShowChart, data.id, true)
      // 初始化进度条图表
      this.initBarChart(data.id, this.current / this.total, '#4f9aff')
      // 获取文件路径中的文件名部分
      const lastSlashIndex = data.filePath.lastIndexOf('/')
      const extractedPart = data.filePath.substring(lastSlashIndex + 1)
      // 开始下载文件的分块
      this.downloadChunk(extractedPart, data)
    },
    // 下载文件的分块方法
    async downloadChunk (fileName, data) {
      const chunkdownloadUrl = `${process.env.VUE_APP_BASE_URL}${process.env.VUE_APP_GATEWAY}${config.service.server}/chunkDownload/${fileName}`
      const token = localStorage.getItem('token')
      const headers = {
        Authorization: `bearer ${token.access_token}`,
        expires_in: token.expires_in,
        refresh_token: token.refresh_token,
        client_id: '20180901'
      }

      try {
        const res = await fetch(chunkdownloadUrl, { method: 'GET', headers })
        if (!res.ok) throw new Error('网络错误')

        const m = 5 * 1024 * 1024 // 每块的大小为5MB
        const size = Number(res.headers.get('Content-Length'))
        this.total = size
        const length = Math.ceil(size / m)

        const downloadPromises = []
        for (let i = 0; i < length; i++) {
          const start = i * m
          const end = (i === length - 1) ? size - 1 : (i + 1) * m - 1
          this.current = end

          downloadPromises.push(this.downloadRange(chunkdownloadUrl, start, end, i, data))
        }

        const results = await Promise.all(downloadPromises)
        const arrBufferList = results.sort((a, b) => a.i - b.i).map(item => new Uint8Array(item.buffer))
        const allBuffer = this.concatenate(Uint8Array, arrBufferList)
        const blob = new Blob([allBuffer])
        const blobUrl = URL.createObjectURL(blob)
        const aTag = document.createElement('a')
        aTag.download = fileName // 设置下载文件名
        aTag.href = blobUrl
        aTag.click() // 触发下载
        URL.revokeObjectURL(blob) // 释放内存
        URL.revokeObjectURL(blobUrl) // 释放内存
      } catch (error) {
        console.error('下载失败:', error)
      }
    },
    // 更新下载进度的方法
    updateProgress (current, total, data) {
      // 计算当前进度
      const progress = current / total
      // 更新历史进度
      if (progress > this.historyNum) { this.historyNum = progress }
      // 更新进度条图表
      this.initBarChart(data.id, this.historyNum, '#4f9aff')
      // 强制更新视图
      this.$forceUpdate()
    },

    // 下载指定范围的方法
    downloadRange (url, start, end, i, data) {
      return new Promise((resolve, reject) => {
        const req = new XMLHttpRequest()
        // 获取存储的token
        var token = localStorage.getItem('token')
        req.open('GET', url, true)
        // 设置请求头
        req.setRequestHeader('range', `bytes=${start}-${end}`)
        req.setRequestHeader('Authorization', 'bearer ' + token.access_token)
        req.setRequestHeader('expires_in', token.expires_in)
        req.setRequestHeader('refresh_token', token.refresh_token)
        req.setRequestHeader('client_id', '20180901')
        req.responseType = 'blob'
        // 请求完成后的处理
        req.onload = () => {
          req.response.arrayBuffer().then(res => {
            resolve({
              i,
              buffer: res
            })
            this.updateProgress(end + 1, this.total, data) // 更新进度
          })
        }
        req.send()
      })
    },
    // 合并多个buffer的方法
    concatenate (ResultConstructor, arrays) {
      const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0)
      const result = new ResultConstructor(totalLength)
      let offset = 0
      arrays.forEach(arr => {
        result.set(arr, offset)
        offset += arr.length
      })
      return result
    }

  }
}
</script>

后端实现#

@Api(tags = "操作接口")
@RestController
@RequestMapping("")
public class TestController {
    
    private static final int BUFFER_SIZE = 8192;
    
    @GetMapping(path = "/chunkDownload/{fileName}")
    @ApiOperation(value = "分片下载文件")
    public void chunkDownload(HttpServletRequest request, HttpServletResponse response, @PathVariable(value = "fileName") String fileName) throws Exception {
        chunkDownloadFile(request, response, fileName);
    }

    /**
     * 分片下载文件
     *
     * @param request 请求
     * @param response  响应
     * @param fileName 文件名称
     * @throws Exception 异常
     */
    private static void chunkDownloadFile(HttpServletRequest request, HttpServletResponse response, String fileName) throws IOException {
        String src = "/data/app";  //基础文件夹路径
        File file = new File(src + fileName);
        long fileSize = file.length();
        response.setContentType("application/octet-stream;charset=UTF-8");
        response.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");
        response.setHeader("Accept-Ranges", "bytes");

        String rangeHeader = request.getHeader("Range");
        if (rangeHeader == null) {
            downloadEntireFile(response, file, fileSize);
        } else {
            downloadPartialFile(response, file, rangeHeader, fileSize);
        }
    }

    /**
     * 下载整个文件
     *
     * @param response 响应
     * @param file     文件
     * @param fileSize 文件大小
     * @throws IOException 异常
     */
    private static void downloadEntireFile(HttpServletResponse response, File file, long fileSize) throws IOException {
        try (InputStream in = new FileInputStream(file);
             OutputStream out = response.getOutputStream()) {
            response.setHeader("Content-Length", String.valueOf(fileSize));
            byte[] buffer = new byte[BUFFER_SIZE];
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
        }
    }

    /**
     * 下载部分文件
     *
     * @param response 响应
     * @param file     文件
     * @param rangeHeader 范围头
     * @param fileSize 文件大小
     * @throws IOException 异常
     */
    private static void downloadPartialFile(HttpServletResponse response, File file, String rangeHeader, long fileSize) throws IOException {
        long start = 0;
        long end = fileSize - 1;
        String[] range = rangeHeader.split("=")[1].split("-");
        if (range.length == 1) {
            start = Long.parseLong(range[0]);
        } else {
            start = Long.parseLong(range[0]);
            end = Long.parseLong(range[1]);
        }
        long contentLength = end - start + 1;
        if(end == fileSize){
            contentLength = end - start;
        }
        response.setHeader("Content-Length", String.valueOf(contentLength));
        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
        response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize);

        try (InputStream in = new FileInputStream(file);
             OutputStream out = response.getOutputStream()) {
            if(start != 0) {
                in.skip(start);
            }
            byte[] buffer = new byte[BUFFER_SIZE];
            int bytesRead;
            long bytesWritten = 0;
            while ((bytesRead = in.read(buffer)) != -1) {
                if (bytesWritten + bytesRead > contentLength) {
                    out.write(buffer, 0, (int) (contentLength - bytesWritten));
                    break;
                } else {
                    out.write(buffer, 0, bytesRead);
                    bytesWritten += bytesRead;
                }
            }
        }
    }
}
posted @   IamHzc  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示
主题色彩