后端代码
package com. jy. jy. controller ; import org. springframework. http. HttpHeaders ;
import org. springframework. http. HttpStatus ;
import org. springframework. http. MediaType ;
import org. springframework. http. ResponseEntity ;
import org. springframework. web. bind. annotation. * ;
import java. io. * ;
import java. util. regex. Pattern ;
import org. springframework. http. * ;
import java. io. IOException ; @RestController
@CrossOrigin ( origins = "*" , maxAge = 3600 )
@RequestMapping ( "/api/common/config/download" )
public class FileDownloadController { private static final String DOWNLOAD_DIR = "/path/to/download/files" ; private static final String FILE_PATH = "D:\\wmxy_repository.rar" ; private static final Pattern RANGE_PATTERN = Pattern . compile ( "bytes=(\\d+)-(\\d*)" ) ; @GetMapping ( "/file" ) public ResponseEntity < byte [ ] > downloadFile ( @RequestParam ( value = "name" , required = false ) String fileName, @RequestHeader ( value = "Range" , required = false ) String rangeHeader) throws IOException { File file = new File ( FILE_PATH ) ; if ( ! file. exists ( ) ) { return ResponseEntity . notFound ( ) . build ( ) ; } long fileSize = file. length ( ) ; HttpHeaders headers = new HttpHeaders ( ) ; headers. setContentType ( MediaType . APPLICATION_OCTET_STREAM ) ; headers. setContentDisposition ( ContentDisposition . attachment ( ) . filename ( fileName) . build ( ) ) ; headers. set ( "Accept-Ranges" , "bytes" ) ; try ( FileInputStream fis = new FileInputStream ( file) ) { if ( rangeHeader == null ) { byte [ ] content = new byte [ ( int ) fileSize] ; fis. read ( content) ; headers. setContentLength ( fileSize) ; return new ResponseEntity < > ( content, headers, HttpStatus . OK ) ; } else { long [ ] range = parseRange ( rangeHeader, fileSize) ; long start = range[ 0 ] ; long end = range[ 1 ] ; long contentLength = end - start + 1 ; if ( start > end || start >= fileSize) { return ResponseEntity . status ( HttpStatus . REQUESTED_RANGE_NOT_SATISFIABLE ) . header ( "Content-Range" , "bytes */" + fileSize) . build ( ) ; } headers. setContentLength ( contentLength) ; headers. set ( "Content-Range" , "bytes " + start + "-" + end + "/" + fileSize) ; byte [ ] buffer = new byte [ ( int ) contentLength] ; fis. skip ( start) ; fis. read ( buffer) ; return new ResponseEntity < > ( buffer, headers, HttpStatus . PARTIAL_CONTENT ) ; } } } private long [ ] parseRange ( String rangeHeader, long fileSize) { long start = 0 ; long end = fileSize - 1 ; try { String [ ] parts = rangeHeader. replace ( "bytes=" , "" ) . split ( "-" ) ; start = Long . parseLong ( parts[ 0 ] ) ; if ( parts. length > 1 && ! parts[ 1 ] . isEmpty ( ) ) { end = Math . min ( Long . parseLong ( parts[ 1 ] ) , fileSize - 1 ) ; } start = Math . max ( start, 0 ) ; end = Math . min ( end, fileSize - 1 ) ; } catch ( Exception e) { start = 0 ; end = fileSize - 1 ; } return new long [ ] { start, end} ; }
}
前端代码
< ! DOCTYPE html>
< html lang= "zh-CN" >
< head> < meta charset= "UTF-8" > < meta name= "viewport" content= "width=device-width, initial-scale=1.0" > < title> 文件分块下载示例< / title> < script src= "https://cdn.tailwindcss.com" > < / script> < link href= "https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel= "stylesheet" > < script> tailwind. config = { theme: { extend: { colors: { primary: '#165D FF ', success: '#00 B42A ', warning: '#FF7D00 ', danger: '#F53F3F ', } , fontFamily: { inter: [ 'Inter' , 'system- ui', ' sans- serif'] , } , } , } } < / script> < style type= "text/tailwindcss" > @layer utilities { . content- auto { content- visibility: auto; } . download- btn { @apply px- 4 py- 2 bg- primary text- white rounded- md transition- all duration- 300 hover: bg- primary/ 90 active: scale- 95 focus: outline- none focus: ring- 2 focus: ring- primary/ 50 ; } . control- btn { @apply px- 3 py- 1.5 rounded- md transition- all duration- 300 hover: bg- gray- 100 active: scale- 95 focus: outline- none; } . progress- bar { @apply h- 2 rounded- full bg- gray- 200 overflow- hidden; } . progress- value { @apply h- full bg- primary transition- all duration- 300 ease- out; } } < / style>
< / head>
< body class = "bg-gray-50 font-inter min-h-screen flex flex-col" > < header class = "bg-white shadow-sm py-4 px-6" > < div class = "container mx-auto flex justify-between items-center" > < h1 class = "text-2xl font-bold text-gray-800" > 文件分块下载< / h1> < / div> < / header> < main class = "flex-grow container mx-auto px-4 py-8" > < div class = "max-w-3xl mx-auto bg-white rounded-lg shadow-md p-6" > < div class = "mb-6" > < h2 class = "text-xl font-semibold text-gray-800 mb-4" > 选择下载文件< / h2> < div class = "grid grid-cols-1 md:grid-cols-2 gap-4" > < div class = "flex flex-col" > < label class = "text-sm font-medium text-gray-700 mb-2" > 文件列表< / label> < select id= "fileSelect" class = "border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary" > < option value= "file1.zip" > 大型文件1. zip ( 2.4 GB ) < / option> < option value= "file2.zip" > 大型文件2. zip ( 1.7 GB ) < / option> < option value= "file3.zip" > 大型文件3. zip ( 3.1 GB ) < / option> < / select> < / div> < div class = "flex flex-col" > < label class = "text-sm font-medium text-gray-700 mb-2" > 块大小< / label> < select id= "chunkSizeSelect" class = "border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary" > < option value= "1048576" > 1 MB < / option> < option value= "5242880" selected> 5 MB < / option> < option value= "10485760" > 10 MB < / option> < option value= "52428800" > 50 MB < / option> < / select> < / div> < / div> < / div> < div id= "downloadSection" class = "hidden" > < div class = "flex items-center justify-between mb-3" > < h3 class = "text-lg font-medium text-gray-800" > 下载进度< / h3> < div class = "flex space-x-2" > < button id= "pauseBtn" class = "control-btn text-warning hidden" > < i class = "fa fa-pause mr-1" > < / i> 暂停< / button> < button id= "resumeBtn" class = "control-btn text-primary hidden" > < i class = "fa fa-play mr-1" > < / i> 继续< / button> < button id= "cancelBtn" class = "control-btn text-danger" > < i class = "fa fa-times mr-1" > < / i> 取消< / button> < / div> < / div> < div class = "mb-3" > < div class = "flex justify-between text-sm text-gray-600 mb-1" > < span id= "fileName" > 大型文件1. zip< / span> < span id= "progressText" > 0 % < / span> < / div> < div class = "progress-bar" > < div id= "progressBar" class = "progress-value" style= "width: 0%" > < / div> < / div> < / div> < div class = "grid grid-cols-2 gap-4 text-sm text-gray-600 mb-4" > < div> < span class = "font-medium" > 已下载: < / span> < span id= "downloadedSize" > 0 MB < / span> < / div> < div> < span class = "font-medium" > 总大小: < / span> < span id= "totalSize" > 2.4 GB < / span> < / div> < div> < span class = "font-medium" > 下载速度: < / span> < span id= "downloadSpeed" > 0 KB / s< / span> < / div> < div> < span class = "font-medium" > 剩余时间: < / span> < span id= "remainingTime" > 计算中. . . </ span> < / div> < / div> < div id= "downloadComplete" class = "hidden text-success mb-4" > < i class = "fa fa-check-circle mr-2" > < / i> 下载完成! < / div> < / div> < div class = "flex justify-center mt-8" > < button id= "startDownloadBtn" class = "download-btn" > < i class = "fa fa-download mr-2" > < / i> 开始下载< / button> < / div> < / div> < / main> < footer class = "bg-gray-800 text-white py-6 px-4" > < div class = "container mx-auto text-center text-sm" > < p> © 文件分块下载示例 | 使用 Tailwind CSS 构建< / p> < / div> < / footer> < script> document. addEventListener ( 'DOMContentLoaded ', ( ) = > { const startDownloadBtn = document. getElementById ( 'startDownloadBtn') ; const pauseBtn = document. getElementById ( 'pauseBtn') ; const resumeBtn = document. getElementById ( 'resumeBtn') ; const cancelBtn = document. getElementById ( 'cancelBtn') ; const downloadSection = document. getElementById ( 'downloadSection') ; const progressBar = document. getElementById ( 'progressBar') ; const progressText = document. getElementById ( 'progressText') ; const downloadedSize = document. getElementById ( 'downloadedSize') ; const totalSize = document. getElementById ( 'totalSize') ; const downloadSpeed = document. getElementById ( 'downloadSpeed') ; const remainingTime = document. getElementById ( 'remainingTime') ; const downloadComplete = document. getElementById ( 'downloadComplete') ; const fileSelect = document. getElementById ( 'fileSelect') ; const chunkSizeSelect = document. getElementById ( 'chunkSizeSelect') ; let isDownloading = false ; let isPaused = false ; let downloadedBytes = 0 ; let totalBytes = 0 ; let chunks = [ ] ; let currentChunk = 0 ; let startTime = 0 ; let lastUpdateTime = 0 ; let timer = null ; let abortController = null ; const fileMap = { 'file1. zip': { size: 5.51 * 1024 * 1024 * 1024 , url: 'http: / / localhost: 89 / api/ common/ config/ download/ file? name= file1. zip' } , 'file2. zip': { size: 1.7 * 1024 * 1024 * 1024 , url: '/ download/ file? name= file2. zip' } , 'file3. zip': { size: 3.1 * 1024 * 1024 * 1024 , url: '/ download/ file? name= file3. zip' } , } ; function formatBytes ( bytes, decimals = 2 ) { if ( bytes == = 0 ) return '0 Bytes '; const k = 1024 ; const dm = decimals < 0 ? 0 : decimals; const sizes = [ 'Bytes' , 'KB' , 'MB' , 'GB' , 'TB' ] ; const i = Math . floor ( Math . log ( bytes) / Math . log ( k) ) ; return parseFloat ( ( bytes / Math . pow ( k, i) ) . toFixed ( dm) ) + ' ' + sizes[ i] ; } function formatTime ( seconds) { if ( seconds < 60 ) { return `${ Math . round ( seconds) } 秒`; } else if ( seconds < 3600 ) { const minutes = Math . floor ( seconds / 60 ) ; const secs = Math . round ( seconds % 60 ) ; return `${ minutes} 分 ${ secs} 秒`; } else { const hours = Math . floor ( seconds / 3600 ) ; const minutes = Math . floor ( ( seconds % 3600 ) / 60 ) ; return `${ hours} 时 ${ minutes} 分`; } } function updateProgress ( ) { const now = Date . now ( ) ; const elapsed = ( now - lastUpdateTime) / 1000 ; lastUpdateTime = now; if ( elapsed <= 0 ) return ; const speed = ( ( downloadedBytes - ( chunks. length > 0 ? chunks. reduce ( ( sum, chunk) = > sum + chunk. size, 0 ) - chunks[ chunks. length - 1 ] . size : 0 ) ) / elapsed) / 1024 ; const remainingBytes = totalBytes - downloadedBytes; const eta = remainingBytes > 0 ? remainingBytes / ( speed * 1024 ) : 0 ; const percent = Math . min ( 100 , Math . round ( ( downloadedBytes / totalBytes) * 100 ) ) ; progressBar. style. width = `${ percent} % `; progressText. textContent = `${ percent} % `; downloadedSize. textContent = formatBytes ( downloadedBytes) ; downloadSpeed. textContent = `${ speed. toFixed ( 1 ) } KB / s`; remainingTime. textContent = formatTime ( eta) ; if ( downloadedBytes >= totalBytes) { finishDownload ( ) ; } } function finishDownload ( ) { isDownloading = false ; isPaused = false ; clearInterval ( timer) ; timer = null ; downloadComplete. classList. remove ( 'hidden' ) ; pauseBtn. classList. add ( 'hidden' ) ; resumeBtn. classList. add ( 'hidden' ) ; const blob = new Blob ( chunks. map ( chunk = > chunk. data) , { type: 'application/ octet- stream' } ) ; const url = URL . createObjectURL ( blob) ; const a = document. createElement ( 'a' ) ; a. href = url; a. download = fileSelect. value; document. body. appendChild ( a) ; a. click ( ) ; document. body. removeChild ( a) ; URL . revokeObjectURL ( url) ; console. log ( '下载完成!' ) ; } async function downloadChunk ( start, end) { if ( ! isDownloading || isPaused) return ; try { abortController = new AbortController ( ) ; const signal = abortController. signal; const headers = { 'Range' : `bytes= ${ start} - ${ end} `} ; const response = await fetch ( fileMap[ fileSelect. value] . url, { method: 'GET' , headers, signal} ) ; if ( ! response. ok) { throw new Error ( `下载失败: ${ response. status} ${ response. statusText} `) ; } const contentRange = response. headers. get ( 'content- range') ; const contentLength = parseInt ( response. headers. get ( 'content- length') , 10 ) ; const arrayBuffer = await response. arrayBuffer ( ) ; chunks. push ( { start, end, size: contentLength, data: arrayBuffer} ) ; downloadedBytes += contentLength; updateProgress ( ) ; currentChunk++ ; if ( currentChunk < chunksInfo. length) { await downloadChunk ( chunksInfo[ currentChunk] . start, chunksInfo[ currentChunk] . end) ; } } catch ( error) { if ( error. name == = 'AbortError ') { console. log ( '下载已取消' ) ; } else { console. error ( '下载出错:' , error) ; alert ( `下载出错: ${ error. message} `) ; } isDownloading = false ; } } async function startDownload ( ) { const selectedFile = fileSelect. value; const chunkSize = parseInt ( chunkSizeSelect. value, 10 ) ; isDownloading = true ; isPaused = false ; downloadedBytes = 0 ; chunks = [ ] ; currentChunk = 0 ; downloadComplete. classList. add ( 'hidden' ) ; totalBytes = fileMap[ selectedFile] . size; totalSize. textContent = formatBytes ( totalBytes) ; downloadSection. classList. remove ( 'hidden' ) ; pauseBtn. classList. remove ( 'hidden' ) ; resumeBtn. classList. add ( 'hidden' ) ; const fileSize = totalBytes; const numChunks = Math . ceil ( fileSize / chunkSize) ; chunksInfo = [ ] ; for ( let i = 0 ; i < numChunks; i++ ) { const start = i * chunkSize; const end = Math . min ( ( i + 1 ) * chunkSize - 1 , fileSize - 1 ) ; chunksInfo. push ( { start, end } ) ; } startTime = Date . now ( ) ; lastUpdateTime = startTime; clearInterval ( timer) ; timer = setInterval ( updateProgress, 1000 ) ; await downloadChunk ( chunksInfo[ 0 ] . start, chunksInfo[ 0 ] . end) ; } function pauseDownload ( ) { isPaused = true ; if ( abortController) { abortController. abort ( ) ; } pauseBtn. classList. add ( 'hidden' ) ; resumeBtn. classList. remove ( 'hidden' ) ; } async function resumeDownload ( ) { isPaused = false ; pauseBtn. classList. remove ( 'hidden' ) ; resumeBtn. classList. add ( 'hidden' ) ; if ( currentChunk < chunksInfo. length) { await downloadChunk ( chunksInfo[ currentChunk] . start, chunksInfo[ currentChunk] . end) ; } } function cancelDownload ( ) { isDownloading = false ; isPaused = false ; if ( abortController) { abortController. abort ( ) ; } clearInterval ( timer) ; timer = null ; downloadSection. classList. add ( 'hidden' ) ; pauseBtn. classList. add ( 'hidden' ) ; resumeBtn. classList. add ( 'hidden' ) ; progressBar. style. width = '0%' ; progressText. textContent = '0%' ; downloadedSize. textContent = '0 MB' ; downloadSpeed. textContent = '0 KB/s' ; remainingTime. textContent = '计算中...' ; chunks = [ ] ; downloadedBytes = 0 ; currentChunk = 0 ; console. log ( '下载已取消' ) ; } startDownloadBtn. addEventListener ( 'click' , startDownload) ; pauseBtn. addEventListener ( 'click' , pauseDownload) ; resumeBtn. addEventListener ( 'click' , resumeDownload) ; cancelBtn. addEventListener ( 'click' , cancelDownload) ; } ) ; < / script>
< / body>
< / html>