Appearance
13.5 实操:用户头像上传
本章节将通过一个完整的实操示例,实现用户头像上传功能。
项目结构
avatar-upload/
├── db.php # 数据库连接文件
├── index.php # 登录页面
├── profile.php # 个人资料页面
├── upload_avatar.php # 头像上传处理
├── logout.php # 退出登录
├── default-avatar.png # 默认头像
└── uploads/ # 上传目录
└── avatars/ # 头像存储目录1. 数据库配置
创建 db.php 文件,用于数据库连接:
php
<?php
// db.php
function getDbConnection() {
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "php_tutorial";
// 创建连接
$conn = new mysqli($servername, $username, $password, $dbname);
// 检查连接
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}
// 设置字符集
$conn->set_charset("utf8mb4");
return $conn;
}
?>2. 数据库表结构
创建 users 表:
sql
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
avatar VARCHAR(255) DEFAULT 'default-avatar.png',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入测试用户
INSERT INTO users (username, email, password) VALUES
('testuser', 'test@example.com', 'password123');3. 登录页面
创建 index.php 文件:
php
<?php
// index.php
session_start();
// 如果已登录,跳转到个人资料页面
if (isset($_SESSION['user_id'])) {
header('Location: profile.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_once 'db.php';
$conn = getDbConnection();
$username = $_POST['username'];
$password = $_POST['password'];
$stmt = $conn->prepare("SELECT id, username, avatar FROM users WHERE username = ? AND password = ?");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$user = $result->fetch_assoc();
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['avatar'] = $user['avatar'];
header('Location: profile.php');
exit;
} else {
$error = '用户名或密码错误';
}
$stmt->close();
$conn->close();
}
?>
<!DOCTYPE html>
<html>
<head>
<title>登录</title>
<style>
body { font-family: Arial, sans-serif; max-width: 400px; margin: 0 auto; padding: 20px; }
h1 { text-align: center; color: #333; }
.form-group { margin: 15px 0; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="text"], input[type="password"] {
padding: 10px; border: 1px solid #ddd; border-radius: 4px; width: 100%; box-sizing: border-box;
}
input[type="submit"] {
padding: 10px 20px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; width: 100%;
}
.error { padding: 10px; margin: 10px 0; border-radius: 4px; background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
</style>
</head>
<body>
<h1>用户登录</h1>
<?php if (!empty($error)): ?>
<div class="error"><?php echo $error; ?></div>
<?php endif; ?>
<form action="" method="post">
<div class="form-group">
<label>用户名: <input type="text" name="username" required></label>
</div>
<div class="form-group">
<label>密码: <input type="password" name="password" required></label>
</div>
<input type="submit" value="登录">
</form>
<p style="text-align: center; margin-top: 20px;">测试账号: testuser / password123</p>
</body>
</html>4. 个人资料页面
创建 profile.php 文件:
php
<?php
// profile.php
session_start();
// 检查是否登录
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
require_once 'db.php';
$conn = getDbConnection();
$userId = $_SESSION['user_id'];
// 获取用户信息
$stmt = $conn->prepare("SELECT username, email, avatar FROM users WHERE id = ?");
$stmt->bind_param("i", $userId);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$user = $result->fetch_assoc();
$avatarUrl = 'uploads/avatars/' . ($user['avatar'] ?: 'default-avatar.png');
if (!file_exists($avatarUrl)) {
$avatarUrl = 'default-avatar.png';
}
} else {
header('Location: index.php');
exit;
}
$stmt->close();
$conn->close();
?>
<!DOCTYPE html>
<html>
<head>
<title>个人资料</title>
<style>
body { font-family: Arial, sans-serif; max-width: 500px; margin: 0 auto; padding: 20px; }
h1 { text-align: center; color: #333; }
.profile { text-align: center; margin: 20px 0; }
.avatar { width: 150px; height: 150px; border-radius: 50%; object-fit: cover; }
.user-info { margin: 20px 0; }
.form-group { margin: 15px 0; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="file"] { padding: 10px; border: 1px solid #ddd; border-radius: 4px; width: 100%; box-sizing: border-box; }
input[type="submit"] { padding: 10px 20px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; }
.message { padding: 10px; margin: 10px 0; border-radius: 4px; }
.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.logout { text-align: center; margin-top: 20px; }
.logout a { color: #dc3545; text-decoration: none; }
.logout a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>个人资料</h1>
<div class="profile">
<img src="<?php echo $avatarUrl; ?>" alt="头像" class="avatar">
<h2><?php echo $user['username']; ?></h2>
<p><?php echo $user['email']; ?></p>
</div>
<?php if (isset($_GET['success'])): ?>
<div class="message success">
<?php echo $_GET['success']; ?>
</div>
<?php endif; ?>
<form action="upload_avatar.php" method="post" enctype="multipart/form-data">
<div class="form-group">
<label>更换头像: <input type="file" name="avatar" accept="image/*" required></label>
</div>
<input type="submit" value="上传头像" name="submit">
</form>
<div class="logout">
<a href="logout.php">退出登录</a>
</div>
</body>
</html>5. 头像上传处理
创建 upload_avatar.php 文件:
php
<?php
// upload_avatar.php
session_start();
// 检查是否登录
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
require_once 'db.php';
$conn = getDbConnection();
$targetDir = "uploads/avatars/";
$maxFileSize = 2 * 1024 * 1024; // 2MB
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$errors = [];
// 确保上传目录存在
if (!is_dir($targetDir)) {
mkdir($targetDir, 0755, true);
}
$userId = $_SESSION['user_id'];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['avatar'])) {
$file = $_FILES['avatar'];
// 检查是否有错误
if ($file['error'] !== 0) {
$errors[] = '上传失败,错误代码: ' . $file['error'];
} else {
// 检查文件大小
if ($file['size'] > $maxFileSize) {
$errors[] = "文件太大,最大允许 2MB";
}
// 检查文件扩展名
$fileExtension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($fileExtension, $allowedExtensions)) {
$errors[] = "只允许上传图片文件(JPG、PNG、GIF、WebP)";
}
// 检查是否为真实图片
if (getimagesize($file['tmp_name']) === false) {
$errors[] = "请上传真实的图片文件";
}
// 处理上传
if (empty($errors)) {
// 生成新文件名
$newFileName = uniqid() . '.' . $fileExtension;
$targetFile = $targetDir . $newFileName;
if (move_uploaded_file($file['tmp_name'], $targetFile)) {
// 更新数据库
$stmt = $conn->prepare("UPDATE users SET avatar = ? WHERE id = ?");
$stmt->bind_param("si", $newFileName, $userId);
if ($stmt->execute()) {
// 更新会话中的头像信息
$_SESSION['avatar'] = $newFileName;
header('Location: profile.php?success=头像上传成功');
exit;
} else {
$errors[] = "更新数据库失败";
}
$stmt->close();
} else {
$errors[] = "上传失败,请重试";
}
}
}
}
$conn->close();
// 如果有错误,跳回个人资料页面
if (!empty($errors)) {
$errorMsg = urlencode(implode('; ', $errors));
header('Location: profile.php?error=' . $errorMsg);
exit;
}
?>6. 退出登录
创建 logout.php 文件:
php
<?php
// logout.php
session_start();
// 清除会话变量
session_unset();
// 销毁会话
session_destroy();
// 跳转到登录页面
header('Location: index.php');
exit;
?>7. 运行项目
- 确保数据库已创建并插入测试用户
- 启动本地服务器
- 访问
http://localhost/avatar-upload/index.php - 使用测试账号登录:
- 用户名: testuser
- 密码: password123
- 上传头像并查看效果
8. 功能扩展
1. 添加图片预览功能
修改 profile.php 文件,添加图片预览功能:
html
<!-- 在表单中添加预览区域 -->
<div class="form-group">
<label>更换头像: <input type="file" name="avatar" accept="image/*" required onchange="previewImage(this);"></label>
<div id="preview-container" style="margin-top: 10px; display: none;">
<img id="preview" src="" alt="预览" style="width: 100px; height: 100px; border-radius: 50%; object-fit: cover;">
</div>
</div>
<script>
function previewImage(input) {
if (input.files && input.files[0]) {
const previewContainer = document.getElementById('preview-container');
const preview = document.getElementById('preview');
previewContainer.style.display = 'block';
const reader = new FileReader();
reader.onload = function(e) {
preview.src = e.target.result;
};
reader.readAsDataURL(input.files[0]);
}
}
</script>2. 添加头像裁剪功能
使用第三方库如 Cropper.js 添加头像裁剪功能:
html
<!-- 在 head 中添加 Cropper.js 的 CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">
<!-- 在 body 底部添加 Cropper.js 的 JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
<!-- 修改表单 -->
<form action="upload_avatar.php" method="post" enctype="multipart/form-data">
<div class="form-group">
<label>更换头像: <input type="file" name="avatar" accept="image/*" required onchange="openCropper(this);"></label>
</div>
<!-- 裁剪模态框 -->
<div id="crop-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.8); z-index: 1000;">
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: white; padding: 20px; border-radius: 8px;">
<h3>裁剪头像</h3>
<div style="width: 300px; height: 300px; overflow: hidden;">
<img id="crop-image" src="" alt="裁剪" style="max-width: 100%;">
</div>
<div style="margin-top: 20px; text-align: center;">
<button type="button" onclick="cancelCrop();" style="padding: 10px 20px; background-color: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 10px;">取消</button>
<button type="button" onclick="cropImage();" style="padding: 10px 20px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">确认</button>
</div>
</div>
</div>
<input type="hidden" name="croppedImage" id="croppedImage">
<input type="submit" value="上传头像" name="submit" style="display: none;">
</form>
<script>
let cropper;
function openCropper(input) {
if (input.files && input.files[0]) {
const cropModal = document.getElementById('crop-modal');
const cropImage = document.getElementById('crop-image');
cropModal.style.display = 'block';
const reader = new FileReader();
reader.onload = function(e) {
cropImage.src = e.target.result;
// 初始化 Cropper.js
cropper = new Cropper(cropImage, {
aspectRatio: 1,
viewMode: 1,
dragMode: 'move',
autoCropArea: 0.8,
cropBoxMovable: true,
cropBoxResizable: true
});
};
reader.readAsDataURL(input.files[0]);
}
}
function cancelCrop() {
const cropModal = document.getElementById('crop-modal');
cropModal.style.display = 'none';
// 销毁 Cropper 实例
if (cropper) {
cropper.destroy();
cropper = null;
}
}
function cropImage() {
if (cropper) {
// 获取裁剪后的图片数据
const canvas = cropper.getCroppedCanvas({
width: 300,
height: 300
});
// 将裁剪后的图片转换为 base64 格式
const croppedImage = canvas.toDataURL('image/jpeg');
// 设置隐藏字段的值
document.getElementById('croppedImage').value = croppedImage;
// 关闭模态框
cancelCrop();
// 提交表单
document.querySelector('form').submit();
}
}
</script>然后修改 upload_avatar.php 文件,处理裁剪后的图片:
php
// 处理裁剪后的图片
if (isset($_POST['croppedImage'])) {
$croppedImage = $_POST['croppedImage'];
// 移除 base64 前缀
$croppedImage = str_replace('data:image/jpeg;base64,', '', $croppedImage);
// 解码 base64 数据
$croppedImage = base64_decode($croppedImage);
// 生成新文件名
$newFileName = uniqid() . '.jpg';
$targetFile = $targetDir . $newFileName;
// 保存裁剪后的图片
if (file_put_contents($targetFile, $croppedImage)) {
// 更新数据库
$stmt = $conn->prepare("UPDATE users SET avatar = ? WHERE id = ?");
$stmt->bind_param("si", $newFileName, $userId);
if ($stmt->execute()) {
// 更新会话中的头像信息
$_SESSION['avatar'] = $newFileName;
header('Location: profile.php?success=头像上传成功');
exit;
} else {
$errors[] = "更新数据库失败";
}
$stmt->close();
} else {
$errors[] = "上传失败,请重试";
}
} else {
// 原有的文件上传处理逻辑
// ...
}3. 添加图片压缩功能
使用 GD 库对上传的图片进行压缩:
php
// 压缩图片
function compressImage($sourcePath, $targetPath, $quality = 80) {
list($sourceWidth, $sourceHeight) = getimagesize($sourcePath);
$sourceImage = imagecreatefromjpeg($sourcePath);
$targetImage = imagecreatetruecolor($sourceWidth, $sourceHeight);
imagecopyresampled($targetImage, $sourceImage, 0, 0, 0, 0, $sourceWidth, $sourceHeight, $sourceWidth, $sourceHeight);
$result = imagejpeg($targetImage, $targetPath, $quality);
imagedestroy($sourceImage);
imagedestroy($targetImage);
return $result;
}
// 使用示例
if (move_uploaded_file($file['tmp_name'], $targetFile)) {
// 压缩图片
compressImage($targetFile, $targetFile, 80);
// 更新数据库
// ...
}9. 注意事项
安全性:
- 验证上传文件的类型和大小
- 重命名文件,避免文件名冲突
- 限制上传目录的权限
- 不要将上传的文件直接存储在 web 根目录外
性能:
- 对上传的图片进行压缩
- 生成缩略图,提高加载速度
用户体验:
- 提供图片预览功能
- 支持图片裁剪
- 显示上传进度
- 提供清晰的错误提示
兼容性:
- 支持不同浏览器的文件上传
- 考虑移动设备的上传体验
- 提供默认头像
练习
- 实现完整的用户头像上传系统
- 添加图片预览和裁剪功能
- 实现图片压缩功能
- 优化用户界面和体验
- 添加多尺寸头像生成(如小、中、大尺寸)
