本文目录导读:

我来为你实现一个完整的PHP分片上传带进度条系统。
前端HTML + JavaScript
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">PHP分片上传 - 进度条</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', sans-serif;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 600px;
margin: 50px auto;
background: #fff;
border-radius: 10px;
padding: 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h2 {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.upload-area {
border: 2px dashed #ddd;
border-radius: 8px;
padding: 40px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
margin-bottom: 20px;
}
.upload-area:hover {
border-color: #007bff;
background: #f8f9fa;
}
.upload-area.dragover {
border-color: #007bff;
background: #e7f1ff;
}
.upload-area i {
font-size: 48px;
color: #007bff;
margin-bottom: 10px;
}
.file-info {
color: #666;
font-size: 14px;
margin-top: 10px;
}
.progress-container {
display: none;
margin-top: 30px;
}
.progress-info {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
font-size: 14px;
}
.progress-bar-container {
background: #e9ecef;
border-radius: 4px;
height: 20px;
overflow: hidden;
}
.progress-bar {
background: linear-gradient(90deg, #007bff, #00bcd4);
height: 100%;
width: 0%;
transition: width 0.3s;
display: flex;
align-items: center;
justify-content: center;
min-width: 30px;
}
.progress-text {
color: #fff;
font-size: 12px;
font-weight: bold;
}
.upload-controls {
margin-top: 20px;
text-align: center;
}
.btn {
padding: 10px 30px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin: 0 10px;
}
.btn-start {
background: #007bff;
color: #fff;
}
.btn-start:hover {
background: #0056b3;
}
.btn-pause {
background: #ffc107;
color: #333;
}
.btn-reset {
background: #dc3545;
color: #fff;
}
.status {
text-align: center;
margin-top: 20px;
padding: 10px;
border-radius: 5px;
}
.status.success {
background: #d4edda;
color: #155724;
}
.status.error {
background: #f8d7da;
color: #721c24;
}
.chunk-info {
font-size: 12px;
color: #666;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="container">
<h2>📁 文件分片上传</h2>
<div class="upload-area" id="uploadArea">
<div class="upload-icon">📦</div>
<div>点击选择或拖拽文件到此处</div>
<div class="file-info" id="fileInfo"></div>
</div>
<input type="file" id="fileInput" style="display:none">
<div class="progress-container" id="progressContainer">
<div class="progress-info">
<span>上传进度:</span>
<span id="progressPercent">0%</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar" id="progressBar">
<span class="progress-text" id="progressText">0%</span>
</div>
</div>
<div class="chunk-info" id="chunkInfo"></div>
</div>
<div class="upload-controls">
<button class="btn btn-start" id="startBtn" disabled>开始上传</button>
<button class="btn btn-pause" id="pauseBtn" disabled>暂停</button>
<button class="btn btn-reset" id="resetBtn" disabled>重置</button>
</div>
<div class="status" id="status"></div>
</div>
<script>
class ChunkUploader {
constructor(options) {
this.uploadUrl = options.uploadUrl;
this.mergeUrl = options.mergeUrl;
this.checkUrl = options.checkUrl;
this.chunkSize = options.chunkSize || 1024 * 1024; // 1MB
this.concurrent = options.concurrent || 3; // 并发数
this.file = null;
this.chunks = [];
this.uploadedChunks = new Set();
this.paused = false;
this.initProcess = 0;
this.elements = {
uploadArea: document.getElementById('uploadArea'),
fileInput: document.getElementById('fileInput'),
progressContainer: document.getElementById('progressContainer'),
progressBar: document.getElementById('progressBar'),
progressText: document.getElementById('progressText'),
progressPercent: document.getElementById('progressPercent'),
chunkInfo: document.getElementById('chunkInfo'),
startBtn: document.getElementById('startBtn'),
pauseBtn: document.getElementById('pauseBtn'),
resetBtn: document.getElementById('resetBtn'),
status: document.getElementById('status'),
fileInfo: document.getElementById('fileInfo')
};
this.bindEvents();
}
bindEvents() {
// 拖拽上传
this.elements.uploadArea.addEventListener('click', () => {
this.elements.fileInput.click();
});
this.elements.uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
this.elements.uploadArea.classList.add('dragover');
});
this.elements.uploadArea.addEventListener('dragleave', () => {
this.elements.uploadArea.classList.remove('dragover');
});
this.elements.uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
this.elements.uploadArea.classList.remove('dragover');
this.handleFileSelect(e.dataTransfer.files[0]);
});
// 文件选择
this.elements.fileInput.addEventListener('change', (e) => {
this.handleFileSelect(e.target.files[0]);
});
// 开始上传
this.elements.startBtn.addEventListener('click', () => {
this.upload();
});
// 暂停
this.elements.pauseBtn.addEventListener('click', () => {
this.pause();
});
// 重置
this.elements.resetBtn.addEventListener('click', () => {
this.reset();
});
}
handleFileSelect(file) {
if (!file) return;
this.file = file;
this.elements.fileInfo.textContent = `${file.name} (${this.formatSize(file.size)})`;
this.elements.uploadArea.style.display = 'none';
this.elements.progressContainer.style.display = 'block';
this.elements.startBtn.disabled = false;
this.elements.resetBtn.disabled = false;
this.elements.pauseBtn.disabled = true;
this.initChunks();
this.updateProgress(0);
}
initChunks() {
const chunkCount = Math.ceil(this.file.size / this.chunkSize);
this.chunks = [];
for (let i = 0; i < chunkCount; i++) {
this.chunks.push(i);
}
this.uploadedChunks.clear();
}
async upload() {
if (!this.file) {
this.elements.status.innerHTML = '⚠️ 请先选择文件';
this.elements.status.className = 'status error';
return;
}
this.paused = false;
this.elements.startBtn.style.display = 'none';
this.elements.pauseBtn.disabled = false;
const fileId = this.getFileId();
try {
// 检查已上传的分片
const checkResult = await this.checkUploadedChunks(fileId);
if (checkResult.uploaded) {
this.uploadedChunks = new Set(checkResult.chunks);
}
// 获取未上传的分片
const pendingChunks = this.chunks.filter(chunk =>
!this.uploadedChunks.has(chunk)
);
if (pendingChunks.length === 0) {
await this.mergeChunks(fileId);
return;
}
// 并发上传
await this.uploadChunks(pendingChunks, fileId);
// 所有分片上传完成,合并
await this.mergeChunks(fileId);
} catch (error) {
console.error('Upload error:', error);
this.elements.status.innerHTML = '❌ 上传失败:' + error.message;
this.elements.status.className = 'status error';
this.elements.startBtn.style.display = 'inline-block';
this.elements.pauseBtn.disabled = true;
}
}
async uploadChunks(chunks, fileId) {
let index = 0;
async function worker() {
while (index < chunks.length && !this.paused) {
const currentIndex = index++;
const chunkIndex = chunks[currentIndex];
if (this.uploadedChunks.has(chunkIndex)) {
continue;
}
const chunk = this.file.slice(
chunkIndex * this.chunkSize,
Math.min((chunkIndex + 1) * this.chunkSize, this.file.size)
);
const formData = new FormData();
formData.append('file', chunk);
formData.append('fileId', fileId);
formData.append('chunkIndex', chunkIndex);
formData.append('chunkCount', this.chunks.length);
try {
await this.uploadSingleChunk(formData);
this.uploadedChunks.add(chunkIndex);
// 更新进度
const progress = (this.uploadedChunks.size / this.chunks.length) * 100;
this.updateProgress(progress);
} catch (error) {
console.error(`Chunk ${chunkIndex} upload failed:`, error);
// 重试一次
try {
await this.uploadSingleChunk(formData);
this.uploadedChunks.add(chunkIndex);
} catch (retryError) {
throw new Error(`分片 ${chunkIndex} 上传失败`);
}
}
}
}
// 启动并发worker
const workers = [];
for (let i = 0; i < this.concurrent; i++) {
workers.push(worker.call(this));
}
await Promise.all(workers);
}
uploadSingleChunk(formData) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', this.uploadUrl);
xhr.timeout = 60000;
xhr.onload = () => {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.success) {
resolve(response);
} else {
reject(new Error('上传失败'));
}
} catch (e) {
reject(e);
}
} else {
reject(new Error('上传错误'));
}
};
xhr.onerror = () => reject(new Error('网络错误'));
xhr.ontimeout = () => reject(new Error('上传超时'));
xhr.send(formData);
});
}
async checkUploadedChunks(fileId) {
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', `${this.checkUrl}?fileId=${fileId}`);
xhr.onload = () => {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
resolve(response);
} catch (e) {
resolve({ uploaded: false, chunks: [] });
}
} else {
resolve({ uploaded: false, chunks: [] });
}
};
xhr.onerror = () => resolve({ uploaded: false, chunks: [] });
xhr.send();
});
}
async mergeChunks(fileId) {
this.elements.status.innerHTML = '🔄 正在合并文件...';
const formData = new FormData();
formData.append('fileId', fileId);
formData.append('fileName', this.file.name);
formData.append('fileSize', this.file.size);
formData.append('chunkCount', this.chunks.length);
const xhr = new XMLHttpRequest();
xhr.open('POST', this.mergeUrl);
return new Promise((resolve, reject) => {
xhr.onload = () => {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.success) {
this.updateProgress(100);
this.elements.status.innerHTML = '✅ 文件上传成功!';
this.elements.status.className = 'status success';
this.elements.startBtn.style.display = 'inline-block';
this.elements.pauseBtn.disabled = true;
resolve(response);
} else {
reject(new Error(response.message || '合并失败'));
}
} catch (e) {
reject(e);
}
} else {
reject(new Error('合并失败'));
}
};
xhr.onerror = () => reject(new Error('网络错误'));
xhr.send(formData);
});
}
pause() {
this.paused = true;
this.elements.startBtn.style.display = 'inline-block';
this.elements.pauseBtn.disabled = true;
this.elements.status.innerHTML = '⏸️ 上传已暂停';
this.elements.status.className = 'status';
}
reset() {
this.paused = true;
this.file = null;
this.chunks = [];
this.uploadedChunks.clear();
this.updateProgress(0);
this.elements.uploadArea.style.display = 'block';
this.elements.progressContainer.style.display = 'none';
this.elements.fileInfo.textContent = '';
this.elements.fileInput.value = '';
this.elements.startBtn.disabled = true;
this.elements.pauseBtn.disabled = true;
this.elements.resetBtn.disabled = true;
this.elements.startBtn.style.display = 'inline-block';
this.elements.status.innerHTML = '';
}
updateProgress(percent) {
this.elements.progressBar.style.width = percent + '%';
this.elements.progressText.textContent = percent.toFixed(1) + '%';
this.elements.progressPercent.textContent = percent.toFixed(1) + '%';
const uploadedSize = Math.round((this.uploadedChunks.size / this.chunks.length) * this.file?.size || 0);
this.elements.chunkInfo.innerHTML = `
已上传:${this.uploadedChunks.size}/${this.chunks.length} 分片<br>
已上传:${this.formatSize(uploadedSize)} / ${this.formatSize(this.file?.size || 0)}
`;
}
getFileId() {
// 生成文件唯一标识
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 10);
return `${timestamp}_${random}`;
}
formatSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
}
// 初始化上传器
const uploader = new ChunkUploader({
uploadUrl: 'upload_chunk.php',
mergeUrl: 'merge_chunks.php',
checkUrl: 'check_chunks.php',
chunkSize: 1024 * 1024, // 1MB
concurrent: 3
});
</script>
</body>
</html>
PHP后端处理
upload_chunk.php (分片上传)
<?php
header('Content-Type: application/json');
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new Exception('非法请求方法');
}
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
throw new Exception('文件上传失败');
}
// 配置
$uploadDir = 'uploads/';
$tempDir = $uploadDir . 'temp/';
// 创建目录
if (!file_exists($tempDir)) {
mkdir($tempDir, 0777, true);
}
$fileId = $_POST['fileId'] ?? '';
$chunkIndex = (int)($_POST['chunkIndex'] ?? 0);
$chunkCount = (int)($_POST['chunkCount'] ?? 0);
if (empty($fileId) || $chunkCount <= 0 || $chunkIndex < 0) {
throw new Exception('参数不完整');
}
// 校验分片数据
$chunkPath = $tempDir . $fileId . '_' . $chunkIndex;
// 保存分片
if (!move_uploaded_file($_FILES['file']['tmp_name'], $chunkPath)) {
throw new Exception('保存分片失败');
}
// 检查是否所有分片都已上传
$completedChunks = 0;
$allChunksCompleted = true;
for ($i = 0; $i < $chunkCount; $i++) {
$tempFile = $tempDir . $fileId . '_' . $i;
if (file_exists($tempFile)) {
$completedChunks++;
} else {
$allChunksCompleted = false;
}
}
echo json_encode([
'success' => true,
'message' => '分片 ' . $chunkIndex . ' 上传成功',
'completedChunks' => $completedChunks,
'allCompleted' => $allChunksCompleted
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
check_chunks.php (检查已上传分片)
<?php
header('Content-Type: application/json');
$fileId = $_GET['fileId'] ?? '';
$uploadedChunks = [];
if (!empty($fileId)) {
$tempDir = 'uploads/temp/';
if (file_exists($tempDir)) {
$pattern = $tempDir . $fileId . '_*';
$files = glob($pattern);
if ($files) {
foreach ($files as $file) {
$basename = basename($file);
$parts = explode('_', $basename);
$chunkIndex = (int)end($parts);
$uploadedChunks[] = $chunkIndex;
}
}
}
}
echo json_encode([
'uploaded' => !empty($uploadedChunks),
'chunks' => $uploadedChunks
]);
merge_chunks.php (合并分片)
<?php
header('Content-Type: application/json');
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new Exception('非法请求方法');
}
$fileId = $_POST['fileId'] ?? '';
$fileName = $_POST['fileName'] ?? '';
$fileSize = (int)($_POST['fileSize'] ?? 0);
$chunkCount = (int)($_POST['chunkCount'] ?? 0);
if (empty($fileId) || empty($fileName) || $chunkCount <= 0) {
throw new Exception('参数不完整');
}
// 配置
$uploadDir = 'uploads/';
$tempDir = $uploadDir . 'temp/';
$finalDir = $uploadDir . 'final/';
// 确保目录存在
if (!file_exists($finalDir)) {
mkdir($finalDir, 0777, true);
}
// 生成安全的文件名
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
$newFileName = date('YmdHis') . '_' . uniqid() . '.' . $extension;
$finalPath = $finalDir . $newFileName;
// 合并文件
$finalFile = fopen($finalPath, 'wb');
if (!$finalFile) {
throw new Exception('无法创建最终文件');
}
$completedSize = 0;
for ($i = 0; $i < $chunkCount; $i++) {
$chunkPath = $tempDir . $fileId . '_' . $i;
if (!file_exists($chunkPath)) {
fclose($finalFile);
unlink($finalPath);
throw new Exception("分片 {$i} 不存在");
}
$chunk = file_get_contents($chunkPath);
if ($chunk === false) {
fclose($finalFile);
unlink($finalPath);
throw new Exception("读取分片 {$i} 失败");
}
fwrite($finalFile, $chunk);
$completedSize += strlen($chunk);
// 删除分片文件
unlink($chunkPath);
}
fclose($finalFile);
// 验证文件大小
if ($completedSize !== $fileSize && $fileSize > 0) {
unlink($finalPath);
throw new Exception('文件大小不匹配');
}
// 清理空目录
if (is_dir($tempDir) && count(glob($tempDir . '*')) === 0) {
rmdir($tempDir);
}
echo json_encode([
'success' => true,
'message' => '文件合并成功',
'filename' => $newFileName,
'size' => $completedSize
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
目录结构
project/
├── index.html # 前端页面
├── upload_chunk.php # 分片上传接口
├── check_chunks.php # 检查分片接口
├── merge_chunks.php # 合并分片接口
└── uploads/ # 上传文件目录
├── temp/ # 分片临时目录
└── final/ # 最终文件目录
使用说明
- 配置:在HTML中设置分片大小(默认1MB)和并发数(默认3)
- 选择文件:点击或拖拽文件到上传区域
- 开始上传:点击"开始上传"按钮
- 暂停/继续:随时可暂停和继续上传
- 断点续传:重新上传时会检查已上传的分片
功能特点
- ✅ 分片上传大文件
- ✅ 实时进度条显示
- ✅ 支持暂停/继续
- ✅ 断点续传
- ✅ 并发上传(提升速度)
- ✅ 拖拽文件上传
- ✅ 错误重试机制
这个系统可以处理GB级别的大文件上传,并提供流畅的用户体验。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播