如何通过一个在线相册项目案例展示PHP处理多文件上传的能力

wen PHP项目 42

本文目录导读:

如何通过一个在线相册项目案例展示PHP处理多文件上传的能力

  1. 项目结构
  2. 配置文件 (config.php)
  3. 上传处理 (upload.php)
  4. 相册展示 (gallery.php)
  5. 样式文件 (css/style.css)
  6. 主页面 (index.php)
  7. JavaScript 增强功能 (js/upload.js)
  8. 关键特性展示
  9. 运行说明

我来通过一个完整的在线相册项目案例,展示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">&times;</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 格式

前端功能

  • 文件预览
  • 上传进度条
  • 拖拽上传
  • 实时反馈

安全措施

  • 文件类型验证
  • 文件大小限制
  • 唯一文件名生成
  • 防止路径遍历

运行说明

  1. 将文件放入 Web 服务器目录(如 Apache 的 htdocs
  2. 确保 uploads/photos/uploads/thumbs/ 目录可写
  3. 访问 http://localhost/online-album/gallery.php

这个项目完整展示了 PHP 处理多文件上传的完整流程,包括前端交互、文件验证、缩略图生成等功能。

抱歉,评论功能暂时关闭!