Appearance
15.1 实战 4:个人博客系统
本章节将实现一个完整的个人博客系统,包括文章列表、文章详情、分类管理、后台登录和文章增删改查功能。
项目结构
personal-blog/
├── db.php # 数据库连接文件
├── index.php # 首页(文章列表)
├── post.php # 文章详情页
├── category.php # 分类页面
├── admin/ # 后台目录
│ ├── index.php # 后台登录页
│ ├── dashboard.php # 后台仪表盘
│ ├── posts.php # 文章管理
│ ├── categories.php # 分类管理
│ ├── add-post.php # 添加文章
│ ├── edit-post.php # 编辑文章
│ └── logout.php # 退出登录
└── css/
└── style.css # 样式文件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. 数据库表结构
创建 categories、posts 和 users 表:
sql
-- 分类表
CREATE TABLE categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE,
slug VARCHAR(50) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 文章表
CREATE TABLE posts (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
content TEXT NOT NULL,
category_id INT,
user_id INT,
featured_image VARCHAR(255),
status ENUM('published', 'draft') DEFAULT 'draft',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(id),
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 用户表
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,
role ENUM('admin', 'author') DEFAULT 'author',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入测试数据
-- 插入分类
INSERT INTO categories (name, slug) VALUES
('技术', 'tech'),
('生活', 'life'),
('学习', 'study'),
('工作', 'work');
-- 插入用户
INSERT INTO users (username, email, password, role) VALUES
('admin', 'admin@example.com', 'password123', 'admin');
-- 插入文章
INSERT INTO posts (title, slug, content, category_id, user_id, status) VALUES
('PHP 入门指南', 'php-guide', 'PHP 是一种广泛使用的开源服务器端脚本语言...', 1, 1, 'published'),
('MySQL 数据库基础', 'mysql-basics', 'MySQL 是一种关系型数据库管理系统...', 1, 1, 'published'),
('生活随笔', 'life-essay', '今天天气很好,出去散步...', 2, 1, 'published'),
('学习心得', 'study-notes', '学习编程需要持之以恒...', 3, 1, 'published');3. 样式文件
创建 css/style.css 文件:
css
/* style.css */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f4f4f4;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background-color: #333;
color: white;
padding: 20px 0;
margin-bottom: 30px;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 24px;
font-weight: bold;
}
nav ul {
list-style: none;
display: flex;
gap: 20px;
}
nav a {
color: white;
text-decoration: none;
transition: color 0.3s ease;
}
nav a:hover {
color: #4CAF50;
}
.main-content {
display: flex;
gap: 30px;
}
.content {
flex: 3;
}
.sidebar {
flex: 1;
}
.post {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-bottom: 20px;
}
.post-title {
font-size: 20px;
font-weight: bold;
margin-bottom: 10px;
}
.post-title a {
color: #333;
text-decoration: none;
transition: color 0.3s ease;
}
.post-title a:hover {
color: #4CAF50;
}
.post-meta {
font-size: 14px;
color: #999;
margin-bottom: 15px;
}
.post-excerpt {
margin-bottom: 15px;
color: #666;
}
.post-category {
display: inline-block;
background-color: #4CAF50;
color: white;
padding: 3px 10px;
border-radius: 12px;
font-size: 12px;
margin-bottom: 15px;
}
.read-more {
display: inline-block;
background-color: #333;
color: white;
padding: 8px 16px;
border-radius: 4px;
text-decoration: none;
font-size: 14px;
transition: background-color 0.3s ease;
}
.read-more:hover {
background-color: #4CAF50;
}
.widget {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-bottom: 20px;
}
.widget h3 {
margin-bottom: 15px;
font-size: 18px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.category-list {
list-style: none;
}
.category-list li {
margin-bottom: 10px;
}
.category-list a {
color: #333;
text-decoration: none;
transition: color 0.3s ease;
}
.category-list a:hover {
color: #4CAF50;
}
.post-detail {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 30px;
}
.post-detail h1 {
font-size: 28px;
margin-bottom: 20px;
}
.post-detail-content {
margin-top: 30px;
line-height: 1.8;
color: #444;
}
.post-detail-content p {
margin-bottom: 20px;
}
.pagination {
margin-top: 30px;
text-align: center;
}
.pagination a {
display: inline-block;
padding: 8px 16px;
margin: 0 5px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 4px;
text-decoration: none;
color: #333;
transition: background-color 0.3s ease;
}
.pagination a:hover {
background-color: #f0f0f0;
}
.pagination .active {
background-color: #4CAF50;
color: white;
border: 1px solid #4CAF50;
}
footer {
background-color: #333;
color: white;
padding: 20px 0;
margin-top: 50px;
text-align: center;
}
/* 后台样式 */
.admin-container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.admin-header {
background-color: #333;
color: white;
padding: 10px 0;
margin-bottom: 30px;
}
.admin-nav {
background-color: #f8f9fa;
padding: 10px;
border-radius: 8px;
margin-bottom: 30px;
}
.admin-nav ul {
list-style: none;
display: flex;
gap: 20px;
}
.admin-nav a {
color: #333;
text-decoration: none;
padding: 5px 10px;
border-radius: 4px;
transition: background-color 0.3s ease;
}
.admin-nav a:hover {
background-color: #e9ecef;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input[type="text"],
.form-group input[type="email"],
.form-group input[type="password"],
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.form-group textarea {
height: 200px;
resize: vertical;
}
.btn {
display: inline-block;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease;
}
.btn-primary {
background-color: #4CAF50;
color: white;
}
.btn-primary:hover {
background-color: #45a049;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
.table {
width: 100%;
border-collapse: collapse;
margin-bottom: 30px;
}
.table th,
.table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.table th {
background-color: #f8f9fa;
font-weight: bold;
}
.table tr:hover {
background-color: #f8f9fa;
}
.alert {
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-danger {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.login-form {
max-width: 400px;
margin: 50px auto;
background-color: #fff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.login-form h2 {
text-align: center;
margin-bottom: 30px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.main-content {
flex-direction: column;
}
.header-content {
flex-direction: column;
gap: 10px;
}
nav ul {
flex-wrap: wrap;
justify-content: center;
}
}4. 首页(文章列表)
创建 index.php 文件:
php
<?php
// index.php
require_once 'db.php';
$conn = getDbConnection();
// 分页设置
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$perPage = 5;
$offset = ($page - 1) * $perPage;
// 获取文章总数
$countSql = "SELECT COUNT(*) as total FROM posts WHERE status = 'published'";
$countResult = $conn->query($countSql);
$countRow = $countResult->fetch_assoc();
$totalPosts = $countRow['total'];
$totalPages = ceil($totalPosts / $perPage);
// 获取文章列表
$sql = "SELECT posts.*, categories.name as category_name FROM posts JOIN categories ON posts.category_id = categories.id WHERE posts.status = 'published' ORDER BY posts.created_at DESC LIMIT ? OFFSET ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ii", $perPage, $offset);
$stmt->execute();
$result = $stmt->get_result();
$posts = [];
if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$posts[] = $row;
}
}
// 获取分类列表
$categorySql = "SELECT * FROM categories";
$categoryResult = $conn->query($categorySql);
$categories = [];
if ($categoryResult->num_rows > 0) {
while ($row = $categoryResult->fetch_assoc()) {
$categories[] = $row;
}
}
$stmt->close();
$conn->close();
?>
<!DOCTYPE html>
<html>
<head>
<title>个人博客</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<div class="container header-content">
<div class="logo">个人博客</div>
<nav>
<ul>
<li><a href="index.php">首页</a></li>
<?php foreach ($categories as $category): ?>
<li><a href="category.php?slug=<?php echo $category['slug']; ?>"><?php echo $category['name']; ?></a></li>
<?php endforeach; ?>
</ul>
</nav>
</div>
</header>
<div class="container main-content">
<div class="content">
<?php if (empty($posts)): ?>
<p>暂无文章</p>
<?php else: ?>
<?php foreach ($posts as $post): ?>
<div class="post">
<div class="post-category"><?php echo $post['category_name']; ?></div>
<h2 class="post-title"><a href="post.php?slug=<?php echo $post['slug']; ?>"><?php echo $post['title']; ?></a></h2>
<div class="post-meta">
发布于: <?php echo $post['created_at']; ?>
</div>
<div class="post-excerpt">
<?php echo substr(strip_tags($post['content']), 0, 200); ?>...
</div>
<a href="post.php?slug=<?php echo $post['slug']; ?>" class="read-more">阅读更多</a>
</div>
<?php endforeach; ?>
<?php endif; ?>
<?php if ($totalPages > 1): ?>
<div class="pagination">
<?php if ($page > 1): ?>
<a href="?page=<?php echo $page - 1; ?>">上一页</a>
<?php endif; ?>
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<a href="?page=<?php echo $i; ?>" class="<?php echo $i == $page ? 'active' : ''; ?>">
<?php echo $i; ?>
</a>
<?php endfor; ?>
<?php if ($page < $totalPages): ?>
<a href="?page=<?php echo $page + 1; ?>">下一页</a>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<div class="sidebar">
<div class="widget">
<h3>分类</h3>
<ul class="category-list">
<?php foreach ($categories as $category): ?>
<li><a href="category.php?slug=<?php echo $category['slug']; ?>"><?php echo $category['name']; ?></a></li>
<?php endforeach; ?>
</ul>
</div>
<div class="widget">
<h3>关于我</h3>
<p>这是一个个人博客,分享技术、生活和学习心得。</p>
</div>
</div>
</div>
<footer>
<div class="container">
<p>© 2024 个人博客. 保留所有权利.</p>
</div>
</footer>
</body>
</html>5. 文章详情页
创建 post.php 文件:
php
<?php
// post.php
require_once 'db.php';
$conn = getDbConnection();
$slug = isset($_GET['slug']) ? $_GET['slug'] : '';
// 获取文章详情
$sql = "SELECT posts.*, categories.name as category_name, categories.slug as category_slug FROM posts JOIN categories ON posts.category_id = categories.id WHERE posts.slug = ? AND posts.status = 'published'";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $slug);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 0) {
header('Location: index.php?error=文章不存在');
exit;
}
$post = $result->fetch_assoc();
// 获取分类列表
$categorySql = "SELECT * FROM categories";
$categoryResult = $conn->query($categorySql);
$categories = [];
if ($categoryResult->num_rows > 0) {
while ($row = $categoryResult->fetch_assoc()) {
$categories[] = $row;
}
}
$stmt->close();
$conn->close();
?>
<!DOCTYPE html>
<html>
<head>
<title><?php echo $post['title']; ?> - 个人博客</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<div class="container header-content">
<div class="logo">个人博客</div>
<nav>
<ul>
<li><a href="index.php">首页</a></li>
<?php foreach ($categories as $category): ?>
<li><a href="category.php?slug=<?php echo $category['slug']; ?>"><?php echo $category['name']; ?></a></li>
<?php endforeach; ?>
</ul>
</nav>
</div>
</header>
<div class="container main-content">
<div class="content">
<div class="post-detail">
<div class="post-category"><a href="category.php?slug=<?php echo $post['category_slug']; ?>"><?php echo $post['category_name']; ?></a></div>
<h1><?php echo $post['title']; ?></h1>
<div class="post-meta">
发布于: <?php echo $post['created_at']; ?>
<?php if ($post['updated_at'] != $post['created_at']): ?>
| 更新于: <?php echo $post['updated_at']; ?>
<?php endif; ?>
</div>
<div class="post-detail-content">
<?php echo nl2br($post['content']); ?>
</div>
</div>
</div>
<div class="sidebar">
<div class="widget">
<h3>分类</h3>
<ul class="category-list">
<?php foreach ($categories as $category): ?>
<li><a href="category.php?slug=<?php echo $category['slug']; ?>"><?php echo $category['name']; ?></a></li>
<?php endforeach; ?>
</ul>
</div>
<div class="widget">
<h3>关于我</h3>
<p>这是一个个人博客,分享技术、生活和学习心得。</p>
</div>
</div>
</div>
<footer>
<div class="container">
<p>© 2024 个人博客. 保留所有权利.</p>
</div>
</footer>
</body>
</html>6. 分类页面
创建 category.php 文件:
php
<?php
// category.php
require_once 'db.php';
$conn = getDbConnection();
$slug = isset($_GET['slug']) ? $_GET['slug'] : '';
// 获取分类信息
$categorySql = "SELECT * FROM categories WHERE slug = ?";
$categoryStmt = $conn->prepare($categorySql);
$categoryStmt->bind_param("s", $slug);
$categoryStmt->execute();
$categoryResult = $categoryStmt->get_result();
if ($categoryResult->num_rows === 0) {
header('Location: index.php?error=分类不存在');
exit;
}
$category = $categoryResult->fetch_assoc();
// 分页设置
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$perPage = 5;
$offset = ($page - 1) * $perPage;
// 获取文章总数
$countSql = "SELECT COUNT(*) as total FROM posts WHERE category_id = ? AND status = 'published'";
$countStmt = $conn->prepare($countSql);
$countStmt->bind_param("i", $category['id']);
$countStmt->execute();
$countRow = $countStmt->get_result()->fetch_assoc();
$totalPosts = $countRow['total'];
$totalPages = ceil($totalPosts / $perPage);
// 获取文章列表
$sql = "SELECT * FROM posts WHERE category_id = ? AND status = 'published' ORDER BY created_at DESC LIMIT ? OFFSET ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("iii", $category['id'], $perPage, $offset);
$stmt->execute();
$result = $stmt->get_result();
$posts = [];
if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$posts[] = $row;
}
}
// 获取所有分类
$allCategoriesSql = "SELECT * FROM categories";
$allCategoriesResult = $conn->query($allCategoriesSql);
$allCategories = [];
if ($allCategoriesResult->num_rows > 0) {
while ($row = $allCategoriesResult->fetch_assoc()) {
$allCategories[] = $row;
}
}
$stmt->close();
$categoryStmt->close();
$countStmt->close();
$conn->close();
?>
<!DOCTYPE html>
<html>
<head>
<title><?php echo $category['name']; ?> - 个人博客</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<div class="container header-content">
<div class="logo">个人博客</div>
<nav>
<ul>
<li><a href="index.php">首页</a></li>
<?php foreach ($allCategories as $cat): ?>
<li><a href="category.php?slug=<?php echo $cat['slug']; ?>"><?php echo $cat['name']; ?></a></li>
<?php endforeach; ?>
</ul>
</nav>
</div>
</header>
<div class="container main-content">
<div class="content">
<h1>分类: <?php echo $category['name']; ?></h1>
<?php if (empty($posts)): ?>
<p>该分类下暂无文章</p>
<?php else: ?>
<?php foreach ($posts as $post): ?>
<div class="post">
<h2 class="post-title"><a href="post.php?slug=<?php echo $post['slug']; ?>"><?php echo $post['title']; ?></a></h2>
<div class="post-meta">
发布于: <?php echo $post['created_at']; ?>
</div>
<div class="post-excerpt">
<?php echo substr(strip_tags($post['content']), 0, 200); ?>...
</div>
<a href="post.php?slug=<?php echo $post['slug']; ?>" class="read-more">阅读更多</a>
</div>
<?php endforeach; ?>
<?php endif; ?>
<?php if ($totalPages > 1): ?>
<div class="pagination">
<?php if ($page > 1): ?>
<a href="?slug=<?php echo $slug; ?>&page=<?php echo $page - 1; ?>">上一页</a>
<?php endif; ?>
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<a href="?slug=<?php echo $slug; ?>&page=<?php echo $i; ?>" class="<?php echo $i == $page ? 'active' : ''; ?>">
<?php echo $i; ?>
</a>
<?php endfor; ?>
<?php if ($page < $totalPages): ?>
<a href="?slug=<?php echo $slug; ?>&page=<?php echo $page + 1; ?>">下一页</a>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<div class="sidebar">
<div class="widget">
<h3>分类</h3>
<ul class="category-list">
<?php foreach ($allCategories as $cat): ?>
<li><a href="category.php?slug=<?php echo $cat['slug']; ?>"><?php echo $cat['name']; ?></a></li>
<?php endforeach; ?>
</ul>
</div>
<div class="widget">
<h3>关于我</h3>
<p>这是一个个人博客,分享技术、生活和学习心得。</p>
</div>
</div>
</div>
<footer>
<div class="container">
<p>© 2024 个人博客. 保留所有权利.</p>
</div>
</footer>
</body>
</html>7. 后台登录页
创建 admin/index.php 文件:
php
<?php
// admin/index.php
session_start();
// 如果已登录,跳转到后台仪表盘
if (isset($_SESSION['user_id'])) {
header('Location: dashboard.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
require_once '../db.php';
$conn = getDbConnection();
$username = $_POST['username'];
$password = $_POST['password'];
if (empty($username) || empty($password)) {
$error = '用户名和密码不能为空';
} else {
$stmt = $conn->prepare("SELECT id, username, role 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['role'] = $user['role'];
header('Location: dashboard.php');
exit;
} else {
$error = '用户名或密码错误';
}
$stmt->close();
}
$conn->close();
}
?>
<!DOCTYPE html>
<html>
<head>
<title>后台登录</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<div class="container">
<div class="login-form">
<h2>后台登录</h2>
<?php if (!empty($error)): ?>
<div class="alert alert-danger"><?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>
<button type="submit" name="login" class="btn btn-primary" style="width: 100%;">登录</button>
</form>
</div>
</div>
</body>
</html>8. 后台仪表盘
创建 admin/dashboard.php 文件:
php
<?php
// admin/dashboard.php
session_start();
// 检查是否登录
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
require_once '../db.php';
$conn = getDbConnection();
// 获取文章统计
$postCount = $conn->query("SELECT COUNT(*) as count FROM posts")->fetch_assoc()['count'];
$publishedCount = $conn->query("SELECT COUNT(*) as count FROM posts WHERE status = 'published'")->fetch_assoc()['count'];
$draftCount = $conn->query("SELECT COUNT(*) as count FROM posts WHERE status = 'draft'")->fetch_assoc()['count'];
// 获取分类统计
$categoryCount = $conn->query("SELECT COUNT(*) as count FROM categories")->fetch_assoc()['count'];
$conn->close();
?>
<!DOCTYPE html>
<html>
<head>
<title>后台仪表盘</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<div class="admin-header">
<div class="container header-content">
<div class="logo">博客后台</div>
<nav>
<ul>
<li><a href="dashboard.php">仪表盘</a></li>
<li><a href="posts.php">文章管理</a></li>
<li><a href="categories.php">分类管理</a></li>
<li><a href="logout.php">退出登录</a></li>
</ul>
</nav>
</div>
</div>
<div class="admin-container">
<h1>仪表盘</h1>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-top: 30px;">
<div class="widget">
<h3>总文章数</h3>
<p style="font-size: 36px; font-weight: bold; color: #4CAF50;"><?php echo $postCount; ?></p>
</div>
<div class="widget">
<h3>已发布</h3>
<p style="font-size: 36px; font-weight: bold; color: #2196F3;"><?php echo $publishedCount; ?></p>
</div>
<div class="widget">
<h3>草稿</h3>
<p style="font-size: 36px; font-weight: bold; color: #FF9800;"><?php echo $draftCount; ?></p>
</div>
<div class="widget">
<h3>分类数</h3>
<p style="font-size: 36px; font-weight: bold; color: #9C27B0;"><?php echo $categoryCount; ?></p>
</div>
</div>
</div>
</body>
</html>9. 文章管理
创建 admin/posts.php 文件:
php
<?php
// admin/posts.php
session_start();
// 检查是否登录
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
require_once '../db.php';
$conn = getDbConnection();
// 删除文章
if (isset($_GET['delete'])) {
$postId = (int)$_GET['delete'];
$stmt = $conn->prepare("DELETE FROM posts WHERE id = ?");
$stmt->bind_param("i", $postId);
if ($stmt->execute()) {
header('Location: posts.php?success=文章删除成功');
exit;
} else {
header('Location: posts.php?error=文章删除失败');
exit;
}
}
// 获取文章列表
$sql = "SELECT posts.*, categories.name as category_name FROM posts JOIN categories ON posts.category_id = categories.id ORDER BY posts.created_at DESC";
$result = $conn->query($sql);
$posts = [];
if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$posts[] = $row;
}
}
$conn->close();
?>
<!DOCTYPE html>
<html>
<head>
<title>文章管理</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<div class="admin-header">
<div class="container header-content">
<div class="logo">博客后台</div>
<nav>
<ul>
<li><a href="dashboard.php">仪表盘</a></li>
<li><a href="posts.php">文章管理</a></li>
<li><a href="categories.php">分类管理</a></li>
<li><a href="logout.php">退出登录</a></li>
</ul>
</nav>
</div>
</div>
<div class="admin-container">
<h1>文章管理</h1>
<?php if (isset($_GET['success'])): ?>
<div class="alert alert-success"><?php echo $_GET['success']; ?></div>
<?php endif; ?>
<?php if (isset($_GET['error'])): ?>
<div class="alert alert-danger"><?php echo $_GET['error']; ?></div>
<?php endif; ?>
<a href="add-post.php" class="btn btn-primary" style="margin-bottom: 20px;">添加文章</a>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>标题</th>
<th>分类</th>
<th>状态</th>
<th>发布时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php if (empty($posts)): ?>
<tr>
<td colspan="6" style="text-align: center;">暂无文章</td>
</tr>
<?php else: ?>
<?php foreach ($posts as $post): ?>
<tr>
<td><?php echo $post['id']; ?></td>
<td><?php echo $post['title']; ?></td>
<td><?php echo $post['category_name']; ?></td>
<td><?php echo $post['status'] === 'published' ? '已发布' : '草稿'; ?></td>
<td><?php echo $post['created_at']; ?></td>
<td>
<a href="edit-post.php?id=<?php echo $post['id']; ?>" class="btn btn-primary" style="padding: 5px 10px; font-size: 14px;">编辑</a>
<a href="?delete=<?php echo $post['id']; ?>" class="btn btn-danger" style="padding: 5px 10px; font-size: 14px; margin-left: 10px;" onclick="return confirm('确定要删除这篇文章吗?');">删除</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</body>
</html>10. 添加文章
创建 admin/add-post.php 文件:
php
<?php
// admin/add-post.php
session_start();
// 检查是否登录
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
require_once '../db.php';
$conn = getDbConnection();
$error = '';
$success = '';
// 获取分类列表
$categorySql = "SELECT * FROM categories";
$categoryResult = $conn->query($categorySql);
$categories = [];
if ($categoryResult->num_rows > 0) {
while ($row = $categoryResult->fetch_assoc()) {
$categories[] = $row;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['submit'])) {
$title = $_POST['title'];
$slug = $_POST['slug'];
$content = $_POST['content'];
$categoryId = $_POST['category_id'];
$status = $_POST['status'];
$userId = $_SESSION['user_id'];
if (empty($title) || empty($slug) || empty($content) || empty($categoryId)) {
$error = '标题、别名、内容和分类不能为空';
} else {
// 检查 slug 是否已存在
$stmt = $conn->prepare("SELECT id FROM posts WHERE slug = ?");
$stmt->bind_param("s", $slug);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$error = '别名已存在,请使用其他别名';
} else {
// 插入文章
$stmt = $conn->prepare("INSERT INTO posts (title, slug, content, category_id, user_id, status) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->bind_param("sssiss", $title, $slug, $content, $categoryId, $userId, $status);
if ($stmt->execute()) {
$success = '文章添加成功';
// 清空表单
$title = $slug = $content = '';
$categoryId = 1;
$status = 'draft';
} else {
$error = '文章添加失败,请重试';
}
}
$stmt->close();
}
}
$conn->close();
?>
<!DOCTYPE html>
<html>
<head>
<title>添加文章</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<div class="admin-header">
<div class="container header-content">
<div class="logo">博客后台</div>
<nav>
<ul>
<li><a href="dashboard.php">仪表盘</a></li>
<li><a href="posts.php">文章管理</a></li>
<li><a href="categories.php">分类管理</a></li>
<li><a href="logout.php">退出登录</a></li>
</ul>
</nav>
</div>
</div>
<div class="admin-container">
<h1>添加文章</h1>
<?php if (!empty($error)): ?>
<div class="alert alert-danger"><?php echo $error; ?></div>
<?php endif; ?>
<?php if (!empty($success)): ?>
<div class="alert alert-success"><?php echo $success; ?></div>
<?php endif; ?>
<form action="" method="post">
<div class="form-group">
<label>标题: <input type="text" name="title" value="<?php echo isset($title) ? htmlspecialchars($title) : ''; ?>" required></label>
</div>
<div class="form-group">
<label>别名: <input type="text" name="slug" value="<?php echo isset($slug) ? htmlspecialchars($slug) : ''; ?>" required></label>
<small>用于URL,建议使用小写字母、数字和连字符</small>
</div>
<div class="form-group">
<label>分类:
<select name="category_id" required>
<?php foreach ($categories as $category): ?>
<option value="<?php echo $category['id']; ?>" <?php echo isset($categoryId) && $categoryId == $category['id'] ? 'selected' : ''; ?>>
<?php echo $category['name']; ?>
</option>
<?php endforeach; ?>
</select>
</label>
</div>
<div class="form-group">
<label>内容: <textarea name="content" required><?php echo isset($content) ? htmlspecialchars($content) : ''; ?></textarea></label>
</div>
<div class="form-group">
<label>状态:
<select name="status">
<option value="draft" <?php echo isset($status) && $status == 'draft' ? 'selected' : ''; ?>>草稿</option>
<option value="published" <?php echo isset($status) && $status == 'published' ? 'selected' : ''; ?>>已发布</option>
</select>
</label>
</div>
<button type="submit" name="submit" class="btn btn-primary">保存</button>
<a href="posts.php" class="btn" style="background-color: #6c757d; color: white; margin-left: 10px;">取消</a>
</form>
</div>
</body>
</html>11. 编辑文章
创建 admin/edit-post.php 文件:
php
<?php
// admin/edit-post.php
session_start();
// 检查是否登录
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
require_once '../db.php';
$conn = getDbConnection();
$postId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
// 获取文章详情
$stmt = $conn->prepare("SELECT * FROM posts WHERE id = ?");
$stmt->bind_param("i", $postId);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 0) {
header('Location: posts.php?error=文章不存在');
exit;
}
$post = $result->fetch_assoc();
// 获取分类列表
$categorySql = "SELECT * FROM categories";
$categoryResult = $conn->query($categorySql);
$categories = [];
if ($categoryResult->num_rows > 0) {
while ($row = $categoryResult->fetch_assoc()) {
$categories[] = $row;
}
}
$error = '';
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['submit'])) {
$title = $_POST['title'];
$slug = $_POST['slug'];
$content = $_POST['content'];
$categoryId = $_POST['category_id'];
$status = $_POST['status'];
if (empty($title) || empty($slug) || empty($content) || empty($categoryId)) {
$error = '标题、别名、内容和分类不能为空';
} else {
// 检查 slug 是否已存在(排除当前文章)
$stmt = $conn->prepare("SELECT id FROM posts WHERE slug = ? AND id != ?");
$stmt->bind_param("si", $slug, $postId);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$error = '别名已存在,请使用其他别名';
} else {
// 更新文章
$stmt = $conn->prepare("UPDATE posts SET title = ?, slug = ?, content = ?, category_id = ?, status = ? WHERE id = ?");
$stmt->bind_param("sssisi", $title, $slug, $content, $categoryId, $status, $postId);
if ($stmt->execute()) {
$success = '文章更新成功';
// 更新变量
$post['title'] = $title;
$post['slug'] = $slug;
$post['content'] = $content;
$post['category_id'] = $categoryId;
$post['status'] = $status;
} else {
$error = '文章更新失败,请重试';
}
}
$stmt->close();
}
}
$conn->close();
?>
<!DOCTYPE html>
<html>
<head>
<title>编辑文章</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<div class="admin-header">
<div class="container header-content">
<div class="logo">博客后台</div>
<nav>
<ul>
<li><a href="dashboard.php">仪表盘</a></li>
<li><a href="posts.php">文章管理</a></li>
<li><a href="categories.php">分类管理</a></li>
<li><a href="logout.php">退出登录</a></li>
</ul>
</nav>
</div>
</div>
<div class="admin-container">
<h1>编辑文章</h1>
<?php if (!empty($error)): ?>
<div class="alert alert-danger"><?php echo $error; ?></div>
<?php endif; ?>
<?php if (!empty($success)): ?>
<div class="alert alert-success"><?php echo $success; ?></div>
<?php endif; ?>
<form action="" method="post">
<div class="form-group">
<label>标题: <input type="text" name="title" value="<?php echo htmlspecialchars($post['title']); ?>" required></label>
</div>
<div class="form-group">
<label>别名: <input type="text" name="slug" value="<?php echo htmlspecialchars($post['slug']); ?>" required></label>
<small>用于URL,建议使用小写字母、数字和连字符</small>
</div>
<div class="form-group">
<label>分类:
<select name="category_id" required>
<?php foreach ($categories as $category): ?>
<option value="<?php echo $category['id']; ?>" <?php echo $post['category_id'] == $category['id'] ? 'selected' : ''; ?>>
<?php echo $category['name']; ?>
</option>
<?php endforeach; ?>
</select>
</label>
</div>
<div class="form-group">
<label>内容: <textarea name="content" required><?php echo htmlspecialchars($post['content']); ?></textarea></label>
</div>
<div class="form-group">
<label>状态:
<select name="status">
<option value="draft" <?php echo $post['status'] == 'draft' ? 'selected' : ''; ?>>草稿</option>
<option value="published" <?php echo $post['status'] == 'published' ? 'selected' : ''; ?>>已发布</option>
</select>
</label>
</div>
<button type="submit" name="submit" class="btn btn-primary">保存</button>
<a href="posts.php" class="btn" style="background-color: #6c757d; color: white; margin-left: 10px;">取消</a>
</form>
</div>
</body>
</html>12. 分类管理
创建 admin/categories.php 文件:
php
<?php
// admin/categories.php
session_start();
// 检查是否登录
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
require_once '../db.php';
$conn = getDbConnection();
// 添加分类
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add'])) {
$name = $_POST['name'];
$slug = $_POST['slug'];
if (empty($name) || empty($slug)) {
$error = '分类名称和别名不能为空';
} else {
// 检查名称是否已存在
$stmt = $conn->prepare("SELECT id FROM categories WHERE name = ?");
$stmt->bind_param("s", $name);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$error = '分类名称已存在';
} else {
// 检查别名是否已存在
$stmt = $conn->prepare("SELECT id FROM categories WHERE slug = ?");
$stmt->bind_param("s", $slug);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$error = '分类别名已存在';
} else {
// 插入分类
$stmt = $conn->prepare("INSERT INTO categories (name, slug) VALUES (?, ?)");
$stmt->bind_param("ss", $name, $slug);
if ($stmt->execute()) {
$success = '分类添加成功';
} else {
$error = '分类添加失败,请重试';
}
}
}
$stmt->close();
}
}
// 删除分类
if (isset($_GET['delete'])) {
$categoryId = (int)$_GET['delete'];
// 检查分类是否被文章使用
$stmt = $conn->prepare("SELECT id FROM posts WHERE category_id = ?");
$stmt->bind_param("i", $categoryId);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
header('Location: categories.php?error=该分类下有文章,无法删除');
exit;
} else {
$stmt = $conn->prepare("DELETE FROM categories WHERE id = ?");
$stmt->bind_param("i", $categoryId);
if ($stmt->execute()) {
header('Location: categories.php?success=分类删除成功');
exit;
} else {
header('Location: categories.php?error=分类删除失败');
exit;
}
}
}
// 获取分类列表
$sql = "SELECT * FROM categories ORDER BY name";
$result = $conn->query($sql);
$categories = [];
if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$categories[] = $row;
}
}
$conn->close();
?>
<!DOCTYPE html>
<html>
<head>
<title>分类管理</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<div class="admin-header">
<div class="container header-content">
<div class="logo">博客后台</div>
<nav>
<ul>
<li><a href="dashboard.php">仪表盘</a></li>
<li><a href="posts.php">文章管理</a></li>
<li><a href="categories.php">分类管理</a></li>
<li><a href="logout.php">退出登录</a></li>
</ul>
</nav>
</div>
</div>
<div class="admin-container">
<h1>分类管理</h1>
<?php if (isset($_GET['success']) || isset($success)): ?>
<div class="alert alert-success"><?php echo isset($success) ? $success : $_GET['success']; ?></div>
<?php endif; ?>
<?php if (isset($_GET['error']) || isset($error)): ?>
<div class="alert alert-danger"><?php echo isset($error) ? $error : $_GET['error']; ?></div>
<?php endif; ?>
<h2>添加分类</h2>
<form action="" method="post" style="margin-bottom: 30px;">
<div style="display: flex; gap: 10px; align-items: end;">
<div class="form-group" style="flex: 1;">
<label>分类名称: <input type="text" name="name" required></label>
</div>
<div class="form-group" style="flex: 1;">
<label>别名: <input type="text" name="slug" required></label>
<small>用于URL,建议使用小写字母、数字和连字符</small>
</div>
<div style="margin-bottom: 15px;">
<button type="submit" name="add" class="btn btn-primary">添加</button>
</div>
</div>
</form>
<h2>分类列表</h2>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>别名</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php if (empty($categories)): ?>
<tr>
<td colspan="5" style="text-align: center;">暂无分类</td>
</tr>
<?php else: ?>
<?php foreach ($categories as $category): ?>
<tr>
<td><?php echo $category['id']; ?></td>
<td><?php echo $category['name']; ?></td>
<td><?php echo $category['slug']; ?></td>
<td><?php echo $category['created_at']; ?></td>
<td>
<a href="?delete=<?php echo $category['id']; ?>" class="btn btn-danger" style="padding: 5px 10px; font-size: 14px;" onclick="return confirm('确定要删除这个分类吗?');">删除</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</body>
</html>13. 退出登录
创建 admin/logout.php 文件:
php
<?php
// admin/logout.php
session_start();
// 清除会话变量
session_unset();
// 销毁会话
session_destroy();
// 跳转到登录页面
header('Location: index.php');
exit;
?>14. 运行项目
- 确保数据库已创建并创建了所需的表,插入了测试数据
- 启动本地服务器
- 访问
http://localhost/personal-blog/index.php查看前台 - 访问
http://localhost/personal-blog/admin/index.php登录后台(用户名: admin, 密码: password123) - 在后台管理文章和分类
15. 功能扩展
- 添加富文本编辑器:使用 CKEditor 或 TinyMCE 等富文本编辑器编辑文章内容
- 添加图片上传功能:支持文章特色图片上传
- 添加评论功能:允许用户在文章下方评论
- 添加标签功能:为文章添加标签
- 添加搜索功能:支持文章搜索
- 添加分页功能:后台文章列表添加分页
- 添加用户管理:支持多用户管理
- 添加主题切换:支持前台主题切换
16. 注意事项
安全性:
- 使用
htmlspecialchars防止 XSS 攻击 - 使用预处理语句防止 SQL 注入
- 验证用户输入
- 使用会话管理用户状态
- 使用
性能:
- 使用分页减少数据传输
- 优化数据库查询
- 合理使用会话
用户体验:
- 提供清晰的错误提示
- 显示操作成功的反馈
- 友好的界面设计
- 响应式设计,适配不同屏幕尺寸
功能完整性:
- 实现文章列表展示
- 实现文章详情页
- 实现分类管理
- 实现后台登录
- 实现文章增删改查
练习
- 实现完整的个人博客系统
- 添加富文本编辑器
- 添加图片上传功能
- 添加评论功能
- 优化用户界面和体验
