分块下载时显示下载进度(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;
}
}
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了