本文目录导读:

- 项目结构
- 配置文件 (config.php)
- 上传处理 (upload.php)
- 相册展示 (gallery.php)
- 样式文件 (css/style.css)
- 主页面 (index.php)
- JavaScript 增强功能 (js/upload.js)
- 关键特性展示
- 运行说明
我来通过一个完整的在线相册项目案例,展示PHP处理多文件上传的能力。
项目结构
online-album/
├── index.php # 相册主页
├── upload.php # 上传处理
├── gallery.php # 相册展示
├── config.php # 配置文件
├── uploads/ # 上传目录
│ ├── photos/ # 原图
│ └── thumbs/ # 缩略图
└── css/ # 样式文件
└── style.css
配置文件 (config.php)
<?php
// 配置文件
define('UPLOAD_DIR', 'uploads/photos/');
define('THUMB_DIR', 'uploads/thumbs/');
define('MAX_FILE_SIZE', 5 * 1024 * 1024); // 5MB
define('ALLOWED_TYPES', ['image/jpeg', 'image/png', 'image/gif', 'image/webp']);
define('MAX_FILES', 10); // 一次最多上传10张
// 允许的文件扩展名
define('ALLOWED_EXTENSIONS', ['jpg', 'jpeg', 'png', 'gif', 'webp']);
// 创建上传目录
if (!file_exists(UPLOAD_DIR)) {
mkdir(UPLOAD_DIR, 0777, true);
}
if (!file_exists(THUMB_DIR)) {
mkdir(THUMB_DIR, 0777, true);
}
// 数据库配置(可选,这里用文件系统存储)
// define('DB_HOST', 'localhost');
// define('DB_USER', 'root');
// define('DB_PASS', '');
// define('DB_NAME', 'online_album');
?>
上传处理 (upload.php)
<?php
require_once 'config.php';
// 设置响应头
header('Content-Type: application/json');
// 错误处理类
class UploadHandler {
private $errors = [];
private $success = [];
public function handleUpload() {
// 检查是否有文件上传
if (empty($_FILES['photos'])) {
return $this->response(false, '没有选择文件');
}
$files = $this->rearrangeFiles($_FILES['photos']);
// 检查文件数量
if (count($files) > MAX_FILES) {
return $this->response(false, '一次最多上传' . MAX_FILES . '张照片');
}
// 处理每个文件
foreach ($files as $index => $file) {
$result = $this->processFile($file, $index);
if ($result['success']) {
$this->success[] = $result;
} else {
$this->errors[] = $result;
}
}
// 返回结果
return $this->response(
empty($this->errors) ? true : (empty($this->success) ? false : true),
$this->getMessage()
);
}
// 重组文件数组(处理多文件上传)
private function rearrangeFiles($files) {
$rearranged = [];
$count = count($files['name']);
for ($i = 0; $i < $count; $i++) {
if ($files['error'][$i] !== UPLOAD_ERR_NO_FILE) {
$rearranged[] = [
'name' => $files['name'][$i],
'type' => $files['type'][$i],
'tmp_name' => $files['tmp_name'][$i],
'error' => $files['error'][$i],
'size' => $files['size'][$i]
];
}
}
return $rearranged;
}
// 处理单个文件
private function processFile($file, $index) {
// 检查错误
if ($file['error'] !== UPLOAD_ERR_OK) {
return [
'success' => false,
'name' => $file['name'],
'error' => $this->getUploadError($file['error'])
];
}
// 验证文件类型
if (!in_array($file['type'], ALLOWED_TYPES)) {
return [
'success' => false,
'name' => $file['name'],
'error' => '不支持的文件类型'
];
}
// 验证文件大小
if ($file['size'] > MAX_FILE_SIZE) {
return [
'success' => false,
'name' => $file['name'],
'error' => '文件大小超过5MB限制'
];
}
// 验证扩展名
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($extension, ALLOWED_EXTENSIONS)) {
return [
'success' => false,
'name' => $file['name'],
'error' => '不允许的文件扩展名'
];
}
// 生成唯一文件名
$newFileName = $this->generateFileName($extension);
$uploadPath = UPLOAD_DIR . $newFileName;
// 移动上传文件
if (!move_uploaded_file($file['tmp_name'], $uploadPath)) {
return [
'success' => false,
'name' => $file['name'],
'error' => '文件保存失败'
];
}
// 创建缩略图
$thumbPath = THUMB_DIR . $newFileName;
$this->createThumbnail($uploadPath, $thumbPath, 200);
return [
'success' => true,
'name' => $file['name'],
'filename' => $newFileName,
'path' => $uploadPath,
'thumb' => $thumbPath
];
}
// 生成唯一文件名
private function generateFileName($extension) {
return uniqid() . '_' . time() . '.' . $extension;
}
// 创建缩略图
private function createThumbnail($source, $destination, $thumbWidth) {
$imageInfo = getimagesize($source);
$width = $imageInfo[0];
$height = $imageInfo[1];
// 计算缩放比例
$ratio = $thumbWidth / $width;
$thumbHeight = $height * $ratio;
// 创建真彩色图像
$thumb = imagecreatetruecolor($thumbWidth, $thumbHeight);
// 根据类型创建源图像
switch ($imageInfo['mime']) {
case 'image/jpeg':
$sourceImage = imagecreatefromjpeg($source);
break;
case 'image/png':
$sourceImage = imagecreatefrompng($source);
imagealphablending($thumb, false);
imagesavealpha($thumb, true);
break;
case 'image/gif':
$sourceImage = imagecreatefromgif($source);
break;
case 'image/webp':
$sourceImage = imagecreatefromwebp($source);
break;
default:
return false;
}
// 调整大小
imagecopyresampled($thumb, $sourceImage, 0, 0, 0, 0,
$thumbWidth, $thumbHeight, $width, $height);
// 保存缩略图
switch ($imageInfo['mime']) {
case 'image/jpeg':
imagejpeg($thumb, $destination, 80);
break;
case 'image/png':
imagepng($thumb, $destination, 8);
break;
case 'image/gif':
imagegif($thumb, $destination);
break;
case 'image/webp':
imagewebp($thumb, $destination, 80);
break;
}
// 释放内存
imagedestroy($sourceImage);
imagedestroy($thumb);
return true;
}
// 获取上传错误信息
private function getUploadError($code) {
switch ($code) {
case UPLOAD_ERR_INI_SIZE:
return '文件超过服务器限制';
case UPLOAD_ERR_FORM_SIZE:
return '文件超过表单限制';
case UPLOAD_ERR_PARTIAL:
return '文件只上传了部分';
case UPLOAD_ERR_NO_TMP_DIR:
return '临时文件夹不可用';
case UPLOAD_ERR_CANT_WRITE:
return '无法写入文件';
default:
return '未知错误';
}
}
// 格式化响应消息
private function getMessage() {
$message = '';
if (!empty($this->success)) {
$message .= '成功上传' . count($this->success) . '张照片。';
}
if (!empty($this->errors)) {
$message .= '失败' . count($this->errors) . '张。';
foreach ($this->errors as $error) {
$message .= '<br>' . htmlspecialchars($error['name']) . ': ' . $error['error'];
}
}
return $message;
}
// 响应
private function response($success, $message) {
return json_encode([
'success' => $success,
'message' => $message,
'data' => [
'success' => $this->success,
'errors' => $this->errors
]
]);
}
}
// 处理上传请求
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['photos'])) {
$handler = new UploadHandler();
echo $handler->handleUpload();
} else {
echo json_encode(['success' => false, 'message' => '无效的请求']);
}
?>
相册展示 (gallery.php)
<?php
require_once 'config.php';
class Gallery {
private $photos = [];
public function loadPhotos() {
$files = scandir(THUMB_DIR);
foreach ($files as $file) {
if ($file !== '.' && $file !== '..') {
$this->photos[] = [
'thumb' => THUMB_DIR . $file,
'original' => UPLOAD_DIR . $file,
'name' => $file,
'size' => filesize(UPLOAD_DIR . $file),
'modified' => filemtime(UPLOAD_DIR . $file)
];
}
}
// 按修改时间排序(最新的在前)
usort($this->photos, function($a, $b) {
return $b['modified'] - $a['modified'];
});
return $this->photos;
}
}
?>
<!DOCTYPE html>
<html>
<head>在线相册</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="container">
<h1>📸 在线相册</h1>
<div class="upload-section">
<form id="uploadForm" enctype="multipart/form-data">
<div class="file-input-wrapper">
<input type="file" name="photos[]" id="photos"
multiple accept="image/*"
onchange="previewFiles(this)">
<label for="photos" class="upload-btn">
选择照片
</label>
</div>
<div id="preview" class="preview-container"></div>
<button type="submit" class="submit-btn" id="submitBtn" disabled>
上传照片
</button>
</form>
<div id="progress" class="progress-bar" style="display:none;">
<div class="progress-fill"></div>
</div>
<div id="message" class="message"></div>
</div>
<div class="gallery-section">
<h2>相册内容</h2>
<div class="gallery-grid">
<?php
$gallery = new Gallery();
$photos = $gallery->loadPhotos();
if (empty($photos)) {
echo '<p class="empty-gallery">相册还是空的,快上传照片吧!</p>';
} else {
foreach ($photos as $photo) {
$fileSize = round($photo['size'] / 1024, 2);
$date = date('Y-m-d H:i', $photo['modified']);
?>
<div class="photo-card" onclick="showOriginal('<?= $photo['original'] ?>')">
<img src="<?= $photo['thumb'] ?>"
alt="<?= htmlspecialchars($photo['name']) ?>">
<div class="photo-info">
<span class="size"><?= $fileSize ?> KB</span>
<span class="date"><?= $date ?></span>
</div>
</div>
<?php
}
}
?>
</div>
</div>
</div>
<!-- 查看原图模态框 -->
<div id="modal" class="modal" onclick="closeModal()">
<span class="close">×</span>
<img class="modal-content" id="modalImage">
</div>
<script src="js/upload.js"></script>
<script>
// 文件预览功能
function previewFiles(input) {
const preview = document.getElementById('preview');
preview.innerHTML = '';
const submitBtn = document.getElementById('submitBtn');
if (input.files.length > 10) {
alert('一次最多上传10张照片');
input.value = '';
submitBtn.disabled = true;
return;
}
if (input.files.length > 0) {
submitBtn.disabled = false;
Array.from(input.files).forEach(file => {
const reader = new FileReader();
const div = document.createElement('div');
div.className = 'preview-item';
reader.onload = function(e) {
div.innerHTML = `
<img src="${e.target.result}" alt="${file.name}">
<span class="file-name">${file.name}</span>
<span class="file-size">${(file.size/1024).toFixed(2)} KB</span>
`;
};
reader.readAsDataURL(file);
preview.appendChild(div);
});
} else {
submitBtn.disabled = true;
}
}
// 显示原图
function showOriginal(src) {
const modal = document.getElementById('modal');
const modalImg = document.getElementById('modalImage');
modal.style.display = 'block';
modalImg.src = src;
}
function closeModal() {
document.getElementById('modal').style.display = 'none';
}
// Ajax上传
document.getElementById('uploadForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const progressBar = document.getElementById('progress');
const progressFill = progressBar.querySelector('.progress-fill');
const message = document.getElementById('message');
const submitBtn = document.getElementById('submitBtn');
// 显示进度条
progressBar.style.display = 'block';
submitBtn.disabled = true;
message.innerHTML = '';
const xhr = new XMLHttpRequest();
// 上传进度
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
progressFill.style.width = percent + '%';
}
};
xhr.onload = function() {
progressBar.style.display = 'none';
submitBtn.disabled = false;
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.success) {
message.className = 'message success';
// 刷新页面显示新照片
setTimeout(() => location.reload(), 1500);
} else {
message.className = 'message error';
}
message.innerHTML = response.message;
} else {
message.className = 'message error';
message.innerHTML = '上传失败,请重试';
}
};
xhr.open('POST', 'upload.php', true);
xhr.send(formData);
});
</script>
</body>
</html>
样式文件 (css/style.css)
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 2.5em;
}
/* 上传区域 */
.upload-section {
background: #f8f9fa;
border-radius: 15px;
padding: 30px;
margin-bottom: 30px;
}
.file-input-wrapper {
position: relative;
text-align: center;
margin-bottom: 20px;
}
.file-input-wrapper input[type="file"] {
display: none;
}
.upload-btn {
display: inline-block;
padding: 15px 30px;
background: #667eea;
color: white;
border-radius: 10px;
cursor: pointer;
font-size: 1.1em;
transition: background 0.3s;
}
.upload-btn:hover {
background: #5a67d8;
}
.preview-container {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin: 20px 0;
justify-content: center;
}
.preview-item {
width: 150px;
padding: 10px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
text-align: center;
}
.preview-item img {
width: 100%;
height: 120px;
object-fit: cover;
border-radius: 5px;
margin-bottom: 5px;
}
.preview-item .file-name {
display: block;
font-size: 0.8em;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.submit-btn {
display: block;
margin: 20px auto 0;
padding: 12px 40px;
background: #48bb78;
color: white;
border: none;
border-radius: 10px;
font-size: 1.1em;
cursor: pointer;
transition: background 0.3s;
}
.submit-btn:hover:not(:disabled) {
background: #38a169;
}
.submit-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
/* 进度条 */
.progress-bar {
width: 100%;
height: 10px;
background: #e2e8f0;
border-radius: 5px;
overflow: hidden;
margin: 20px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 5px;
transition: width 0.3s;
width: 0%;
}
/* 消息 */
.message {
padding: 15px;
border-radius: 10px;
margin: 20px 0;
text-align: center;
}
.message.success {
background: #c6f6d5;
color: #276749;
}
.message.error {
background: #fed7d7;
color: #9b2c2c;
}
/* 相册网格 */
.gallery-section {
margin-top: 30px;
}
.gallery-section h2 {
color: #333;
margin-bottom: 20px;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
.photo-card {
background: white;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.3s, box-shadow 0.3s;
}
.photo-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
}
.photo-card img {
width: 100%;
height: 200px;
object-fit: cover;
}
.photo-info {
padding: 15px;
display: flex;
justify-content: space-between;
background: #f8f9fa;
font-size: 0.9em;
color: #666;
}
.empty-gallery {
text-align: center;
color: #999;
padding: 50px;
font-size: 1.2em;
}
/* 模态框 */
.modal {
display: none;
position: fixed;
z-index: 1000;
padding-top: 50px;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
}
.modal-content {
margin: auto;
display: block;
max-width: 90%;
max-height: 90%;
}
.close {
position: absolute;
top: 15px;
right: 35px;
color: #f1f1f1;
font-size: 40px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: #bbb;
}
主页面 (index.php)
<?php
// 简单路由,重定向到相册页面
header('Location: gallery.php');
exit;
?>
JavaScript 增强功能 (js/upload.js)
// 拖拽上传功能
class DragDropUpload {
constructor() {
this.dropZone = document.querySelector('.upload-section');
this.init();
}
init() {
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
this.dropZone.addEventListener(eventName, this.preventDefaults, false);
});
['dragenter', 'dragover'].forEach(eventName => {
this.dropZone.addEventListener(eventName, this.highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
this.dropZone.addEventListener(eventName, this.unhighlight, false);
});
this.dropZone.addEventListener('drop', this.handleDrop, false);
}
preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
highlight() {
this.classList.add('highlight');
}
unhighlight() {
this.classList.remove('highlight');
}
handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
const fileInput = document.getElementById('photos');
// 创建新的FileList
const dataTransfer = new DataTransfer();
Array.from(files).forEach(file => {
if (file.type.startsWith('image/')) {
dataTransfer.items.add(file);
}
});
fileInput.files = dataTransfer.files;
// 触发预览
const event = new Event('change');
fileInput.dispatchEvent(event);
}
}
// 初始化
document.addEventListener('DOMContentLoaded', function() {
new DragDropUpload();
});
关键特性展示
多文件上传处理
- 使用
multiple属性允许一次选择多个文件 - 重组
$_FILES数组结构 - 逐个验证和处理文件
文件验证
// 验证文件类型
if (!in_array($file['type'], ALLOWED_TYPES)) {
// 拒绝上传
}
// 验证文件大小
if ($file['size'] > MAX_FILE_SIZE) {
// 拒绝上传
}
// 验证文件扩展名
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
缩略图生成
- 自动创建缩略图
- 保持图片比例
- 支持 JPEG、PNG、GIF、WebP 格式
前端功能
- 文件预览
- 上传进度条
- 拖拽上传
- 实时反馈
安全措施
- 文件类型验证
- 文件大小限制
- 唯一文件名生成
- 防止路径遍历
运行说明
- 将文件放入 Web 服务器目录(如 Apache 的
htdocs) - 确保
uploads/photos/和uploads/thumbs/目录可写 - 访问
http://localhost/online-album/gallery.php
这个项目完整展示了 PHP 处理多文件上传的完整流程,包括前端交互、文件验证、缩略图生成等功能。