init: 我的博客项目

This commit is contained in:
Your Name
2026-09-09 19:30:18 +08:00
commit 76c241f032
34 changed files with 3420 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# 本地上传数据(不提交)
uploads/
# 日志
*.log
# 系统/编辑器文件
Thumbs.db
.DS_Store
.vscode/
.idea/
+15
View File
@@ -0,0 +1,15 @@
# 启用 gzip 压缩
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/css text/xml application/javascript application/json
</IfModule>
# 静态资源浏览器缓存
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 7 days"
ExpiresByType application/javascript "access plus 7 days"
ExpiresByType image/jpeg "access plus 30 days"
ExpiresByType image/png "access plus 30 days"
ExpiresByType image/gif "access plus 30 days"
ExpiresByType image/webp "access plus 30 days"
</IfModule>
+19
View File
@@ -0,0 +1,19 @@
<?php
require_once __DIR__ . "/includes/config.php";
require_once __DIR__ . "/includes/functions.php";
$pageName = "about";
$pageTitle = "关于";
record_visit("about");
require_once __DIR__ . "/includes/header.php";
?>
<div class="card glass about-card">
<h1>关于</h1>
<p>欢迎访问我的博客。这是一个极简液态玻璃风格的个人博客系统,基于 PHP + MySQL 构建。</p>
<h2>技术栈</h2>
<p>前端使用原生 HTML / CSS / JavaScript,配合 backdrop-filter 实现液态玻璃视觉效果。后端使用 PHP(PDO 预处理防止 SQL 注入),数据库使用 MySQL。</p>
<h2>功能</h2>
<p>支持文章发布、分类、标签与按月归档,提供全文搜索与 Markdown 渲染,内置评论系统,配备文章、分类、标签、评论的后台管理,并支持明暗主题切换。</p>
<h2>设计</h2>
<p>界面采用极简黑白灰配色,无装饰符号,玻璃材质贯穿导航、卡片、侧栏与表单,明暗双主题由 CSS 变量驱动。</p>
</div>
<?php require_once __DIR__ . "/includes/footer.php"; ?>
+43
View File
@@ -0,0 +1,43 @@
<?php
require_once __DIR__ . "/../includes/config.php";
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../includes/auth.php";
require_admin();
$adminPage = isset($adminPage) ? $adminPage : "";
?>
<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo isset($pageTitle) ? e($pageTitle) . " - 后台" : "后台管理"; ?></title>
<link rel="stylesheet" href="<?php echo e(SITE_URL); ?>assets/css/style.css">
<link rel="stylesheet" href="<?php echo e(SITE_URL); ?>assets/css/admin.css">
</head>
<body class="admin-body">
<div class="bg"></div>
<nav class="glass-nav">
<div class="nav-brand"><a href="<?php echo e(SITE_URL); ?>admin/index.php">我的博客 后台</a></div>
<div class="nav-links">
<a class="nav-link" href="<?php echo e(SITE_URL); ?>index.php" target="_blank">查看前台</a>
<a class="glass-btn primary" href="<?php echo e(SITE_URL); ?>logout.php">退出</a>
</div>
</nav>
<div class="container admin-layout">
<aside class="admin-aside">
<div class="side-panel glass">
<nav class="admin-menu">
<div class="menu-title">内容管理</div>
<a class="menu-item<?php echo $adminPage==="articles"?" active":""; ?>" href="<?php echo e(SITE_URL); ?>admin/article_list.php">文章管理</a>
<a class="menu-item<?php echo $adminPage==="comments"?" active":""; ?>" href="<?php echo e(SITE_URL); ?>admin/comment.php">评论管理</a>
<div class="menu-sep"></div>
<div class="menu-title">分类体系</div>
<a class="menu-item<?php echo $adminPage==="categories"?" active":""; ?>" href="<?php echo e(SITE_URL); ?>admin/category.php">分类管理</a>
<a class="menu-item<?php echo $adminPage==="tags"?" active":""; ?>" href="<?php echo e(SITE_URL); ?>admin/tag.php">标签管理</a>
<div class="menu-sep"></div>
<div class="menu-title">总览</div>
<a class="menu-item<?php echo $adminPage==="dash"?" active":""; ?>" href="<?php echo e(SITE_URL); ?>admin/index.php">仪表盘</a>
</nav>
</div>
</aside>
<main class="admin-main">
+18
View File
@@ -0,0 +1,18 @@
</main>
</div>
<div class="footnote">
<?php echo e(SITE_NAME); ?> 后台管理系统
</div>
<div class="modal-mask" id="confirmModal" style="display:none;">
<div class="modal glass">
<div class="modal-title">确认操作</div>
<div class="modal-text" id="confirmText"></div>
<div class="modal-actions">
<button class="glass-btn" id="confirmCancel" type="button">取消</button>
<button class="glass-btn primary" id="confirmOk" type="button">确认</button>
</div>
</div>
</div>
<script src="<?php echo e(SITE_URL); ?>assets/js/main.js"></script>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../includes/auth.php";
require_admin();
$id = isset($_GET["id"]) ? (int)$_GET["id"] : 0;
if ($id) {
db()->prepare("DELETE FROM article_tags WHERE article_id=?")->execute([$id]);
db()->prepare("DELETE FROM comments WHERE article_id=?")->execute([$id]);
db()->prepare("DELETE FROM articles WHERE id=?")->execute([$id]);
}
header("Location: article_list.php?deleted=1"); exit;
+104
View File
@@ -0,0 +1,104 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../includes/auth.php";
$adminPage = "articles";
$pageTitle = "编辑文章";
require __DIR__ . "/_layout.php";
$id = isset($_GET["id"]) ? (int)$_GET["id"] : 0;
$article = null;
if ($id) {
$stmt = db()->prepare("SELECT * FROM articles WHERE id=?");
$stmt->execute([$id]);
$article = $stmt->fetch();
if (!$article) { echo '<div class="msg">文章不存在或已删除</div>'; echo '<p><a href="article_list.php">返回列表</a></p>'; require __DIR__ . "/_tail.php"; exit; }
}
$cats = get_categories();
$allTags = get_all_tags();
$curTags = $article ? array_column(get_article_tags($article["id"]), "id") : [];
$msg = "";
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$title = trim($_POST["title"] ?? "");
$slug = trim($_POST["slug"] ?? "");
$category_id = (int)($_POST["category_id"] ?? 0);
$excerpt = trim($_POST["excerpt"] ?? "");
$content = $_POST["content"] ?? "";
$status = ($_POST["status"] ?? "published") === "draft" ? "draft" : "published";
$tags = $_POST["tags"] ?? []; if (!is_array($tags)) { $tags = []; }
if ($title === "" || $content === "") {
$msg = '<div class="msg">标题和内容不能为空</div>';
} else {
if ($slug === "") { $slug = slugify($title); }
if ($msg === "") {
$user_id = current_user()["id"];
if ($id) {
$s = db()->prepare("UPDATE articles SET title=?, slug=?, category_id=?, excerpt=?, content=?, status=? WHERE id=?");
$s->execute([$title, $slug, $category_id ?: null, $excerpt, $content, $status, $id]);
} else {
$s = db()->prepare("INSERT INTO articles (user_id, title, slug, category_id, excerpt, content, status, views) VALUES (?,?,?,?,?,?,?,0)");
$s->execute([$user_id, $title, $slug, $category_id ?: null, $excerpt, $content, $status]);
$id = (int)db()->lastInsertId();
}
db()->prepare("DELETE FROM article_tags WHERE article_id=?")->execute([$id]);
if ($tags) {
$si = db()->prepare("INSERT IGNORE INTO article_tags (article_id, tag_id) VALUES (?,?)");
foreach ($tags as $tid) { $si->execute([$id, (int)$tid]); }
}
header("Location: article_list.php?ok=1"); exit;
}
}
}
?>
<div class="admin-head"><h1><?php echo $id ? "编辑文章" : "写文章"; ?></h1></div>
<?php echo $msg; ?>
<div class="card glass form-card">
<form method="post">
<div class="field">
<label>标题</label>
<input class="input" type="text" name="title" value="<?php echo e($article["title"] ?? ""); ?>" required>
</div>
<div class="field">
<label>别名(slug,留空自动生成)</label>
<input class="input" type="text" name="slug" value="<?php echo e($article["slug"] ?? ""); ?>" placeholder="english-slug">
</div>
<div class="form-row">
<div class="field">
<label>分类</label>
<select class="input" name="category_id">
<option value="0">未分类</option>
<?php foreach($cats as $c): ?><option value="<?php echo $c["id"]; ?>" <?php echo ($article["category_id"] ?? 0)==$c["id"]?"selected":""; ?>><?php echo e($c["name"]); ?></option><?php endforeach; ?>
</select>
</div>
<div class="field">
<label>状态</label>
<select class="input" name="status">
<option value="published" <?php echo ($article["status"] ?? "published")==="published"?"selected":""; ?>>发布</option>
<option value="draft" <?php echo ($article["status"] ?? "")==="draft"?"selected":""; ?>>草稿</option>
</select>
</div>
</div>
<div class="field">
<label>摘要</label>
<textarea class="input" name="excerpt" rows="2"><?php echo e($article["excerpt"] ?? ""); ?></textarea>
</div>
<div class="field">
<label>标签</label>
<div>
<?php foreach($allTags as $t): ?>
<label class="tag-check"><input type="checkbox" name="tags[]" value="<?php echo $t["id"]; ?>" <?php echo in_array($t["id"],$curTags)?"checked":""; ?>> <?php echo e($t["name"]); ?></label>
<?php endforeach; ?>
<?php if(!$allTags): ?><span class="text-muted">暂无标签,可先在标签管理中创建</span><?php endif; ?>
</div>
</div>
<div class="field">
<label>内容(支持 Markdown)</label>
<textarea class="input code" name="content" rows="16" required><?php echo e($article["content"] ?? ""); ?></textarea>
</div>
<button class="glass-btn primary" type="submit">保存</button>
<a class="glass-btn" href="article_list.php">返回</a>
</form>
</div>
<?php require __DIR__ . "/_tail.php"; ?>
+48
View File
@@ -0,0 +1,48 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../includes/auth.php";
$adminPage = "articles";
$pageTitle = "文章管理";
require __DIR__ . "/_layout.php";
$page = isset($_GET["page"]) ? max(1,(int)$_GET["page"]) : 1;
$per = 15;
$total = (int)db()->query("SELECT COUNT(*) FROM articles")->fetchColumn();
$pages = max(1,(int)ceil($total/$per));
$page = min($page,$pages);
$off = ($page-1)*$per;
$stmt = db()->prepare("SELECT a.id,a.title,a.views,a.created_at,c.name AS cat, COUNT(t.id) AS tagcount FROM articles a LEFT JOIN categories c ON a.category_id=c.id LEFT JOIN article_tags at ON at.article_id=a.id LEFT JOIN tags t ON t.id=at.tag_id GROUP BY a.id ORDER BY a.created_at DESC LIMIT ? OFFSET ?");
$stmt->bindValue(1,$per,PDO::PARAM_INT);
$stmt->bindValue(2,$off,PDO::PARAM_INT);
$stmt->execute();
$list = $stmt->fetchAll();
?>
<div class="admin-head">
<h1>文章管理</h1>
<a class="glass-btn primary" href="article_edit.php">写文章</a>
</div>
<?php if (isset($_GET["deleted"])) { ?><div class="msg ok">删除成功</div><?php } ?>
<div class="card glass">
<table class="admin-table">
<thead><tr><th style="width:40px">ID</th><th class="title">标题</th><th>分类</th><th>标签</th><th>阅读</th><th>日期</th><th>操作</th></tr></thead>
<tbody>
<?php if (!$list): ?><tr><td colspan="7" class="text-muted">暂无文章</td></tr><?php endif; ?>
<?php foreach ($list as $a): ?>
<tr>
<td><?php echo $a["id"]; ?></td>
<td class="title"><?php echo e($a["title"]); ?></td>
<td class="text-muted"><?php echo e($a["cat"] ?: "未分类"); ?></td>
<td><span class="badge-count"><?php echo $a["tagcount"]; ?></span></td>
<td><?php echo $a["views"]; ?></td>
<td class="text-muted"><?php echo e(substr($a["created_at"],0,10)); ?></td>
<td><div class="actions"><a href="article_edit.php?id=<?php echo $a["id"]; ?>">编辑</a><a href="article_delete.php?id=<?php echo $a["id"]; ?>" data-confirm="确定删除该文章?">删除</a></div></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="pagination">
<?php for($i=1;$i<=$pages;$i++): ?><a class="<?php echo $i===$page?"current":""; ?>" href="?page=<?php echo $i; ?>"><?php echo $i; ?></a><?php endfor; ?>
</div>
</div>
<?php require __DIR__ . "/_tail.php"; ?>
+55
View File
@@ -0,0 +1,55 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../includes/auth.php";
$adminPage = "categories";
$pageTitle = "分类管理";
require __DIR__ . "/_layout.php";
$msg = "";
if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["name"])) {
$name = trim($_POST["name"]);
$slug = slugify($_POST["slug"] ?? "");
if ($name === "") { $msg = '<div class="msg">分类名不能为空</div>'; }
else {
$s = db()->prepare("INSERT INTO categories (name, slug) VALUES (?,?)");
$s->execute([$name, $slug]);
header("Location: category.php?ok=1"); exit;
}
}
if (isset($_GET["del"])) {
$cid = (int)$_GET["del"];
db()->prepare("UPDATE articles SET category_id=NULL WHERE category_id=?")->execute([$cid]);
db()->prepare("DELETE FROM categories WHERE id=?")->execute([$cid]);
header("Location: category.php?deleted=1"); exit;
}
$list = db()->query("SELECT c.*, (SELECT COUNT(*) FROM articles a WHERE a.category_id=c.id) AS cnt FROM categories c ORDER BY c.id DESC")->fetchAll();
?>
<div class="admin-head"><h1>分类管理</h1></div>
<?php echo $msg; ?><?php if (isset($_GET["deleted"])) { ?><div class="msg ok">删除成功</div><?php } ?>
<div class="card glass form-card" style="margin-bottom:20px">
<h3>新增分类</h3>
<form method="post">
<div class="form-row">
<div class="field"><label>名称</label><input class="input" type="text" name="name" required></div>
<div class="field"><label>别名(slug,可选)</label><input class="input" type="text" name="slug" placeholder="english-slug"></div>
</div>
<button class="glass-btn primary" type="submit">添加</button>
</form>
</div>
<div class="card glass">
<table class="admin-table">
<thead><tr><th>ID</th><th>名称</th><th>别名</th><th>文章数</th><th>操作</th></tr></thead>
<tbody>
<?php foreach($list as $c): ?>
<tr>
<td><?php echo $c["id"]; ?></td>
<td><?php echo e($c["name"]); ?></td>
<td class="text-muted"><?php echo e($c["slug"]); ?></td>
<td><?php echo $c["cnt"]; ?></td>
<td><div class="actions"><a href="?del=<?php echo $c["id"]; ?>" data-confirm="删除分类不会删除其下文章,仅取消关联,继续?">删除</a></div></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php require __DIR__ . "/_tail.php"; ?>
+48
View File
@@ -0,0 +1,48 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../includes/auth.php";
$adminPage = "comments";
$pageTitle = "评论管理";
require __DIR__ . "/_layout.php";
if (isset($_GET["del"])) {
$cid = (int)$_GET["del"];
db()->prepare("DELETE FROM comments WHERE id=?")->execute([$cid]);
header("Location: comment.php?deleted=1"); exit;
}
$page = isset($_GET["page"]) ? max(1,(int)$_GET["page"]) : 1;
$per = 15;
$total = (int)db()->query("SELECT COUNT(*) FROM comments")->fetchColumn();
$pages = max(1,(int)ceil($total/$per));
$page = min($page,$pages);
$off = ($page-1)*$per;
$stmt = db()->prepare("SELECT cm.*, a.title AS atitle FROM comments cm LEFT JOIN articles a ON a.id=cm.article_id ORDER BY cm.created_at DESC LIMIT ? OFFSET ?");
$stmt->bindValue(1,$per,PDO::PARAM_INT);
$stmt->bindValue(2,$off,PDO::PARAM_INT);
$stmt->execute();
$list = $stmt->fetchAll();
?>
<div class="admin-head"><h1>评论管理</h1></div>
<?php if (isset($_GET["deleted"])) { ?><div class="msg ok">删除成功</div><?php } ?>
<div class="card glass">
<table class="admin-table">
<thead><tr><th style="width:40px">ID</th><th>评论内容</th><th>昵称</th><th>文章</th><th>时间</th><th>操作</th></tr></thead>
<tbody>
<?php if(!$list): ?><tr><td colspan="6" class="text-muted">暂无评论</td></tr><?php endif; ?>
<?php foreach($list as $c): ?>
<tr>
<td><?php echo $c["id"]; ?></td>
<td><?php echo e(mb_strimwidth($c["content"],0,60,"...")); ?></td>
<td><?php echo e($c["author"]); ?></td>
<td class="text-muted"><?php echo e(mb_strimwidth($c["atitle"] ?? "已删除",0,20,"...")); ?></td>
<td class="text-muted"><?php echo e($c["created_at"]); ?></td>
<td><div class="actions"><a href="?del=<?php echo $c["id"]; ?>" data-confirm="确定删除该评论?">删除</a></div></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="pagination">
<?php for($i=1;$i<=$pages;$i++): ?><a class="<?php echo $i===$page?"current":""; ?>" href="?page=<?php echo $i; ?>"><?php echo $i; ?></a><?php endfor; ?>
</div>
</div>
<?php require __DIR__ . "/_tail.php"; ?>
+46
View File
@@ -0,0 +1,46 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../includes/auth.php";
$adminPage = "dash";
$pageTitle = "仪表盘";
require __DIR__ . "/_layout.php";
$articles = (int)db()->query("SELECT COUNT(*) FROM articles")->fetchColumn();
$comments = (int)db()->query("SELECT COUNT(*) FROM comments")->fetchColumn();
$categories = (int)db()->query("SELECT COUNT(*) FROM categories")->fetchColumn();
$tags = (int)db()->query("SELECT COUNT(*) FROM tags")->fetchColumn();
$views = (int)db()->query("SELECT COALESCE(SUM(views),0) FROM articles")->fetchColumn();
$recent = db()->query("SELECT a.id, a.title, a.created_at, c.name AS cat FROM articles a LEFT JOIN categories c ON a.category_id=c.id ORDER BY a.created_at DESC LIMIT 8")->fetchAll();
?>
<div class="admin-head">
<h1>仪表盘</h1>
<a class="glass-btn primary" href="article_edit.php">写文章</a>
</div>
<div class="stat-grid">
<div class="card glass stat-card"><div class="num"><?php echo $articles; ?></div><div class="label">文章</div></div>
<div class="card glass stat-card"><div class="num"><?php echo $comments; ?></div><div class="label">评论</div></div>
<div class="card glass stat-card"><div class="num"><?php echo $views; ?></div><div class="label">总阅读</div></div>
<div class="card glass stat-card"><div class="num"><?php echo $categories; ?></div><div class="label">分类</div></div>
<div class="card glass stat-card"><div class="num"><?php echo $tags; ?></div><div class="label">标签</div></div>
</div>
<div class="card glass">
<h3>最近文章</h3>
<table class="admin-table">
<thead><tr><th>标题</th><th>分类</th><th>发布时间</th><th>操作</th></tr></thead>
<tbody>
<?php if (!$recent): ?><tr><td colspan="4" class="text-muted">暂无文章</td></tr><?php endif; ?>
<?php foreach ($recent as $a): ?>
<tr>
<td><?php echo e($a["title"]); ?></td>
<td class="text-muted"><?php echo e($a["cat"] ?: "未分类"); ?></td>
<td class="text-muted"><?php echo e($a["created_at"]); ?></td>
<td><div class="actions"><a href="article_edit.php?id=<?php echo $a["id"]; ?>">编辑</a><a href="article_delete.php?id=<?php echo $a["id"]; ?>" data-confirm="确定删除该文章?">删除</a></div></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php require __DIR__ . "/_tail.php"; ?>
+55
View File
@@ -0,0 +1,55 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../includes/auth.php";
$adminPage = "tags";
$pageTitle = "标签管理";
require __DIR__ . "/_layout.php";
$msg = "";
if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["name"])) {
$name = trim($_POST["name"]);
$slug = slugify($_POST["slug"] ?? "");
if ($name === "") { $msg = '<div class="msg">标签名不能为空</div>'; }
else {
$s = db()->prepare("INSERT INTO tags (name, slug) VALUES (?,?)");
$s->execute([$name, $slug]);
header("Location: tag.php?ok=1"); exit;
}
}
if (isset($_GET["del"])) {
$tid = (int)$_GET["del"];
db()->prepare("DELETE FROM article_tags WHERE tag_id=?")->execute([$tid]);
db()->prepare("DELETE FROM tags WHERE id=?")->execute([$tid]);
header("Location: tag.php?deleted=1"); exit;
}
$list = db()->query("SELECT t.*, (SELECT COUNT(*) FROM article_tags at WHERE at.tag_id=t.id) AS cnt FROM tags t ORDER BY t.id DESC")->fetchAll();
?>
<div class="admin-head"><h1>标签管理</h1></div>
<?php echo $msg; ?><?php if (isset($_GET["deleted"])) { ?><div class="msg ok">删除成功</div><?php } ?>
<div class="card glass form-card" style="margin-bottom:20px">
<h3>新增标签</h3>
<form method="post">
<div class="form-row">
<div class="field"><label>名称</label><input class="input" type="text" name="name" required></div>
<div class="field"><label>别名(slug,可选)</label><input class="input" type="text" name="slug" placeholder="english-slug"></div>
</div>
<button class="glass-btn primary" type="submit">添加</button>
</form>
</div>
<div class="card glass">
<table class="admin-table">
<thead><tr><th>ID</th><th>名称</th><th>别名</th><th>使用次数</th><th>操作</th></tr></thead>
<tbody>
<?php foreach($list as $t): ?>
<tr>
<td><?php echo $t["id"]; ?></td>
<td><?php echo e($t["name"]); ?></td>
<td class="text-muted"><?php echo e($t["slug"]); ?></td>
<td><?php echo $t["cnt"]; ?></td>
<td><div class="actions"><a href="?del=<?php echo $t["id"]; ?>" data-confirm="删除标签将解除所有文章的该标签关联,继续?">删除</a></div></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php require __DIR__ . "/_tail.php"; ?>
+48
View File
@@ -0,0 +1,48 @@
<?php
require_once __DIR__ . "/includes/config.php";
require_once __DIR__ . "/includes/functions.php";
$pageName = "archive";
$pageTitle = "归档";
record_visit("archive");
$ym = isset($_GET["ym"]) ? trim($_GET["ym"]) : "";
$archives = get_archives();
$perPage = 10;
if ($ym) {
$st = db()->prepare("SELECT COUNT(*) c FROM articles WHERE status=? AND DATE_FORMAT(created_at,'%Y年%m月')=?");
$st->execute(["published",$ym]);
} else {
$st = db()->query("SELECT COUNT(*) c FROM articles WHERE status='published'");
}
$pg = paginate((int)$st->fetch()["c"], isset($_GET["page"])?(int)$_GET["page"]:1, $perPage);
$sql = "SELECT a.*, c.name cat_name FROM articles a LEFT JOIN categories c ON a.category_id=c.id WHERE a.status=? ";
$params = ["published"];
if ($ym) { $sql .= "AND DATE_FORMAT(a.created_at,'%Y年%m月')=? "; $params[] = $ym; }
$sql .= "ORDER BY a.created_at DESC LIMIT ? OFFSET ?";
$stmt = db()->prepare($sql);
$i = 1;
foreach ($params as $p) { $stmt->bindValue($i++, $p, PDO::PARAM_STR); }
$stmt->bindValue($i++, $pg["perPage"], PDO::PARAM_INT);
$stmt->bindValue($i, $pg["offset"], PDO::PARAM_INT);
$stmt->execute();
$articles = $stmt->fetchAll();
require_once __DIR__ . "/includes/header.php";
?>
<div class="page-head">
<h1>归档</h1>
<div class="desc"><?php echo $ym ? "浏览 " . e($ym) . " 的文章" : "全部文章按月归档"; ?></div>
</div>
<div class="card glass" style="padding:16px;margin-bottom:20px;">
<div class="chip-row">
<?php foreach ($archives as $a) { ?>
<a class="glass-chip<?php echo $ym===$a["ym"]?" active":""; ?>" href="?ym=<?php echo e($a["ym"]); ?>"><?php echo e($a["ym"]); ?> (<?php echo (int)$a["cnt"]; ?>)</a>
<?php } ?>
</div>
</div>
<?php foreach ($articles as $a) { ?>
<article class="card glass clickable" onclick="location.href='<?php echo e(SITE_URL); ?>article.php?id=<?php echo (int)$a["id"]; ?>'">
<div class="card-meta"><span><?php echo e(substr($a["created_at"],0,10)); ?></span><span><?php echo (int)$a["views"]; ?> 次阅读</span></div>
<h2><?php echo e($a["title"]); ?></h2>
</article>
<?php } ?>
<?php if ($pg["pages"]>1) { ?><div class="pagination"><?php for($i=1;$i<=$pg["pages"];$i++){ ?><a class="page-btn<?php echo $i===$pg["page"]?" active":""; ?>" href="?ym=<?php echo e($ym); ?>&page=<?php echo $i; ?>"><?php echo $i; ?></a><?php } ?></div><?php } ?>
<?php require_once __DIR__ . "/includes/footer.php"; ?>
+69
View File
@@ -0,0 +1,69 @@
<?php
require_once __DIR__ . "/includes/config.php";
require_once __DIR__ . "/includes/functions.php";
require_once __DIR__ . "/includes/auth.php";
$id = isset($_GET["id"]) ? (int)$_GET["id"] : 0;
$stmt = db()->prepare("SELECT a.*, c.name cat_name, u.username FROM articles a LEFT JOIN categories c ON a.category_id=c.id JOIN users u ON a.user_id=u.id WHERE a.id=? AND a.status=?");
$stmt->execute([$id, "published"]);
$article = $stmt->fetch();
if (!$article) { header("Location: " . SITE_URL . "index.php"); exit; }
$pageTitle = $article["title"];
$pageName = "";
db()->prepare("UPDATE articles SET views=views+1 WHERE id=?")->execute([$id]);
record_visit("article/" . $id);
$tags = get_article_tags($id);
$err = ""; $ok = "";
if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["comment"])) {
$author = trim($_POST["author"] ?? "");
$content = trim($_POST["content"] ?? "");
$parent = isset($_POST["parent_id"]) ? (int)$_POST["parent_id"] : 0;
if ($author === "" || $content === "") { $err = "昵称和内容不能为空"; }
else {
$st = db()->prepare("INSERT INTO comments (article_id, parent_id, author, content, status) VALUES (?,?,?,?,'approved')");
$st->execute([$id, $parent ?: null, $author, $content]);
$ok = "评论已提交";
}
}
$cmtStmt = db()->prepare("SELECT * FROM comments WHERE article_id=? AND status=? ORDER BY created_at ASC");
$cmtStmt->execute([$id, "approved"]);
$comments = $cmtStmt->fetchAll();
require_once __DIR__ . "/includes/header.php";
?>
<article class="card glass article-card">
<h1><?php echo e($article["title"]); ?></h1>
<div class="article-meta">
<span><?php echo e($article["cat_name"] ?: "未分类"); ?></span>
<span><?php echo e(substr($article["created_at"],0,10)); ?></span>
<span><?php echo (int)$article["views"]; ?> 次阅读</span>
<span>作者: <?php echo e($article["username"]); ?></span>
</div>
<div class="article-body"><?php echo markdown($article["content"]); ?></div>
<?php if ($tags) { ?>
<div class="card-meta" style="margin-top:24px;">
<?php foreach ($tags as $t) { ?>
<a class="glass-chip" href="<?php echo e(SITE_URL); ?>tag.php?id=<?php echo (int)$t["id"]; ?>"><?php echo e($t["name"]); ?></a>
<?php } ?>
</div>
<?php } ?>
</article>
<div class="card glass comments">
<h3>评论 (<?php echo count($comments); ?>)</h3>
<?php if ($ok) { ?><div class="msg"><?php echo e($ok); ?></div><?php } ?>
<?php if ($err) { ?><div class="alert error"><?php echo e($err); ?></div><?php } ?>
<?php foreach ($comments as $c) { ?>
<div class="comment-item glass<?php echo $c["parent_id"] ? " reply" : ""; ?>">
<div class="comment-head"><span><?php echo e($c["author"]); ?></span><span><?php echo e($c["created_at"]); ?></span></div>
<div class="comment-body"><?php echo nl2br(e($c["content"])); ?></div>
</div>
<?php } ?>
<?php if (!$comments) { ?><p style="color:var(--text-muted);font-size:14px;">暂无评论</p><?php } ?>
<form method="post" class="comment-form" style="margin-top:20px;">
<input type="hidden" name="comment" value="1">
<div class="field"><label>昵称</label><input class="glass-input" name="author" required></div>
<div class="field"><label>内容</label><textarea class="glass-input" name="content" required></textarea></div>
<button class="glass-btn primary">发表评论</button>
</form>
</div>
<?php require_once __DIR__ . "/includes/footer.php"; ?>
+103
View File
@@ -0,0 +1,103 @@
.admin-body { }
.admin-layout { display:flex; align-items:flex-start; gap:24px; }
.admin-aside { position:sticky; top:88px; align-self:flex-start; width:224px; flex-shrink:0; }
.admin-main { flex:1; min-width:0; }
.admin-menu { padding:12px 10px; }
.admin-menu .menu-title { padding:6px 14px 8px; font-size:11px; letter-spacing:2px; color:var(--text-muted); }
.admin-menu a.menu-item {
display:block; padding:11px 14px; border-radius:12px; margin-bottom:3px;
color:var(--text-secondary); font-size:14px; transition:background .2s,color .2s;
}
.admin-menu a.menu-item:hover { background:var(--glass-bg-strong); color:var(--text-primary); }
.admin-menu a.menu-item.active { background:var(--text-primary); color:var(--bg-0); font-weight:600; }
.admin-menu .menu-sep { height:1px; background:var(--glass-border); margin:10px 6px; }
.admin-head { display:flex; justify-content:space-between; align-items:center; margin-bottom:24px; flex-wrap:wrap; gap:12px; }
.admin-head h1 { font-size:24px; letter-spacing:.5px; }
.admin-head .sub { color:var(--text-muted); font-size:13px; margin-top:4px; }
.stat-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:16px; margin-bottom:26px; }
.stat-card { padding:22px 18px; text-align:center; position:relative; overflow:hidden; }
.stat-card .num { font-size:34px; font-weight:800; letter-spacing:-1px; }
.stat-card .label { color:var(--text-muted); font-size:13px; margin-top:6px; }
.stat-card .foot { font-size:12px; color:var(--text-muted); margin-top:8px; }
.table-wrap { overflow-x:auto; }
.admin-table th.title, .admin-table td.title { max-width:340px; overflow:hidden; text-overflow:ellipsis; }
.admin-table { width:100%; border-collapse:collapse; }
.admin-table th, .admin-table td { padding:11px 10px; text-align:left; border-bottom:1px solid var(--glass-border); font-size:14px; vertical-align:middle; white-space:nowrap; }
.admin-table th { font-weight:600; color:var(--text-secondary); white-space:nowrap; font-size:13px; }
.admin-table tbody tr { transition:background .15s; }
.admin-table tbody tr:hover { background:var(--glass-bg-strong); }
.admin-table tbody tr:last-child td { border-bottom:none; }
.admin-table .thumb { width:72px; height:44px; object-fit:cover; border-radius:8px; display:block; }
.admin-table .num-col { text-align:center; }
.actions a, .actions button { margin-right:10px; font-size:13px; color:var(--text-secondary); background:none; border:none; cursor:pointer; padding:0; }
.actions a:hover, .actions button:hover { color:var(--text-primary); }
.actions .danger { color:#c0392b; }
[data-theme="dark"] .actions .danger { color:#ff8a8a; }
.pagination { display:flex; gap:8px; align-items:center; margin-top:18px; flex-wrap:wrap; }
.pagination a, .pagination span { padding:7px 13px; border-radius:10px; font-size:13px; border:1px solid var(--glass-border); background:var(--glass-bg); color:var(--text-secondary); }
.pagination a:hover { color:var(--text-primary); }
.pagination .current { background:var(--text-primary); color:var(--bg-0); font-weight:600; border-color:var(--text-primary); }
.admin-form .field { margin-bottom:18px; }
.admin-form .field label { display:block; font-weight:600; margin-bottom:8px; font-size:14px; }
.admin-form .field .hint { font-size:12px; color:var(--text-muted); margin-top:6px; }
.admin-form .form-row { display:flex; gap:16px; }
.admin-form .form-row > div { flex:1; }
.admin-form textarea.input, .admin-form .input { width:100%; padding:11px 14px; border-radius:12px; border:1px solid var(--glass-border); background:var(--glass-bg); color:var(--text-primary); font-size:14px; outline:none; transition:.2s; font-family:inherit; }
.admin-form .input:focus { border-color:var(--text-primary); }
.admin-form textarea.input { resize:vertical; min-height:140px; line-height:1.7; }
.admin-form .input.code { font-family:ui-monospace,"Cascadia Code",Consolas,monospace; font-size:13px; }
.admin-form select.input { appearance:none; -webkit-appearance:none; }
.admin-form .btn-row { display:flex; gap:10px; margin-top:6px; }
.tag-check { display:inline-flex; align-items:center; gap:6px; margin:0 14px 10px 0; font-size:14px; color:var(--text-secondary); cursor:pointer; }
.tag-check input { width:16px; height:16px; accent-color:var(--text-primary); }
.msg { padding:13px 16px; border-radius:12px; margin-bottom:16px; background:var(--glass-bg-strong); border:1px solid var(--glass-border); font-size:14px; }
.msg.ok { border-color:rgba(34,139,34,.4); color:#2e7d32; }
[data-theme="dark"] .msg.ok { color:#8fd18f; }
.msg.err { border-color:rgba(192,57,43,.4); color:#c0392b; }
[data-theme="dark"] .msg.err { color:#ff8a8a; }
.card-title { display:flex; justify-content:space-between; align-items:center; margin-bottom:16px; }
.card-title h3 { font-size:16px; }
.text-muted { color:var(--text-muted); }
.empty { text-align:center; color:var(--text-muted); padding:56px 20px; font-size:14px; }
@media (max-width:900px){
.admin-aside { display:none; }
.admin-layout { display:block; }
}
.admin-main .card:hover { transform:none; }
.admin-main .card::after { display:none; }
.admin-main .card { margin-bottom:20px; }
.admin-head .head-right { display:flex; gap:10px; align-items:center; }
/* 后台输入框:清晰可见的边框与背景(浅色主题下白边框不可见) */
.admin-main .input,
.admin-main select.input,
.admin-main textarea.input {
width:100%; padding:11px 14px; border-radius:12px;
border:1px solid var(--input-border);
background:var(--glass-bg-strong);
color:var(--text-primary); font-size:14px; outline:none; transition:.2s; font-family:inherit;
}
.admin-main .input::placeholder { color:var(--text-muted); }
.admin-main .input:focus { border-color:var(--text-primary); box-shadow:0 0 0 3px var(--glass-border); }
.admin-main textarea.input { resize:vertical; min-height:140px; line-height:1.7; }
.admin-main .input.code { font-family:ui-monospace,"Cascadia Code",Consolas,monospace; font-size:13px; }
/* 自定义确认框 */
.modal-mask { position:fixed; inset:0; z-index:200; background:rgba(15,18,28,0.42); display:flex; align-items:center; justify-content:center; padding:24px; }
.modal { width:100%; max-width:360px; padding:28px; }
.modal .modal-title { font-size:17px; font-weight:700; margin-bottom:14px; }
.modal .modal-text { color:var(--text-secondary); font-size:14px; line-height:1.7; margin-bottom:24px; }
.modal .modal-actions { display:flex; justify-content:flex-end; gap:10px; }
+178
View File
@@ -0,0 +1,178 @@
:root {
--bg-0:#ffffff;
--bg-gradient: linear-gradient(160deg,#ffffff 0%,#f5f6fa 55%,#e9ecf3 100%);
--bg-glow: radial-gradient(circle at 50% -10%,rgba(255,255,255,0.35),transparent 55%);
--glass-bg: rgba(255,255,255,0.55);
--glass-bg-strong: rgba(255,255,255,0.8);
--glass-border: rgba(255,255,255,0.9);
--input-border: rgba(20,20,32,0.3);
--glass-highlight: rgba(255,255,255,1);
--glass-blur: 26px;
--glass-radius: 18px;
--text-primary:#141419; --text-secondary:rgba(20,20,32,0.62); --text-muted:rgba(20,20,32,0.42);
--shadow: 0 10px 36px rgba(31,45,84,0.12);
}
[data-theme="dark"] {
--bg-0:#0e0f13;
--bg-gradient: linear-gradient(160deg,#0e0f13 0%,#12141a 55%,#171a22 100%);
--bg-glow: radial-gradient(circle at 50% -10%,rgba(255,255,255,0.05),transparent 50%);
--glass-bg: rgba(255,255,255,0.06);
--glass-bg-strong: rgba(255,255,255,0.12);
--glass-border: rgba(255,255,255,0.14);
--input-border: rgba(255,255,255,0.35);
--glass-highlight: rgba(255,255,255,0.22);
--glass-blur: 26px;
--glass-radius: 18px;
--text-primary:#eef0f5; --text-secondary:rgba(255,255,255,0.66); --text-muted:rgba(255,255,255,0.4);
--shadow: 0 10px 36px rgba(0,0,0,0.5);
}
* { margin:0; padding:0; box-sizing:border-box; }
html { -webkit-text-size-adjust:100%; }
body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif; color:var(--text-primary); min-height:100vh; background:var(--bg-gradient); transition:background .4s; }
a { color:inherit; text-decoration:none; }
.bg { position:fixed; inset:0; z-index:-1; background:var(--bg-gradient); }
.bg::before { content:""; position:absolute; inset:-10%; background:var(--bg-glow); }
.glass { background:var(--glass-bg); backdrop-filter:blur(var(--glass-blur)) saturate(1.5); -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(1.5); border:1px solid var(--glass-border); border-radius:var(--glass-radius); box-shadow:inset 0 1px 0 var(--glass-highlight), var(--shadow); }
.glass-nav { position:fixed; top:0; left:0; right:0; z-index:50; display:flex; align-items:center; justify-content:space-between; padding:0 28px; height:64px; background:var(--glass-bg); backdrop-filter:blur(var(--glass-blur)) saturate(1.6); -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(1.6); border-bottom:1px solid var(--glass-border); box-shadow:inset 0 1px 0 var(--glass-highlight); }
.nav-brand { font-weight:800; font-size:20px; letter-spacing:1px; color:var(--text-primary); }
.nav-links { display:flex; gap:6px; align-items:center; }
.nav-link { padding:8px 14px; border-radius:10px; color:var(--text-secondary); font-size:14px; transition:.2s; }
.nav-link:hover,.nav-link.active { background:var(--glass-bg-strong); color:var(--text-primary); }
.glass-btn { display:inline-block; padding:8px 16px; border-radius:12px; border:1px solid var(--glass-border); background:var(--glass-bg-strong); color:var(--text-primary); cursor:pointer; font-size:14px; transition:.2s; }
.glass-btn:hover { transform:translateY(-1px); }
.glass-btn.primary { background:var(--text-primary); color:var(--bg-0); border-color:transparent; }
.glass-chip { display:inline-block; padding:3px 12px; border-radius:999px; font-size:12px; background:var(--glass-bg); border:1px solid var(--glass-border); color:var(--text-secondary); }
.glass-input { width:100%; padding:12px 16px; border-radius:12px; border:1px solid var(--input-border); background:var(--glass-bg); color:var(--text-primary); font-size:14px; outline:none; transition:.2s; }
.glass-input:focus { border-color:var(--text-primary); box-shadow:0 0 0 3px var(--input-border); }
.glass-input::placeholder{ color:var(--text-muted); }
textarea.glass-input { resize:vertical; min-height:120px; font-family:inherit; }
.glass-select { padding:12px; border-radius:12px; border:1px solid var(--glass-border); background:var(--glass-bg); color:var(--text-primary); font-size:14px; outline:none; }
.theme-toggle { border:none; background:transparent; color:var(--text-secondary); font-size:14px; cursor:pointer; padding:8px 10px; }
.theme-toggle:hover { color:var(--text-primary); }
.container { max-width:1080px; margin:0 auto; padding:88px 24px 60px; }
.layout { display:grid; grid-template-columns:1fr 300px; gap:24px; }
@media (max-width:820px){ .layout{grid-template-columns:1fr;} .sidebar{display:none;} }
.sidebar { position:sticky; top:88px; align-self:start; display:flex; flex-direction:column; gap:20px; }
.side-panel { padding:20px; }
.side-panel h3 { font-size:15px; margin-bottom:14px; }
.chip-row { display:flex; flex-wrap:wrap; gap:8px; }
.chip-row .glass-chip { transition:.15s; }
.chip-row .glass-chip:hover { transform:translateY(-1px); color:var(--text-primary); }
.arch-item { display:flex; justify-content:space-between; padding:9px 0; border-bottom:1px dashed var(--glass-border); font-size:14px; color:var(--text-secondary); }
.arch-item:last-child{ border-bottom:none; }
.arch-item a:hover{ color:var(--text-primary); }
.hero { text-align:center; padding:40px 0 30px; }
.hero h1 { font-size:40px; font-weight:800; letter-spacing:2px; }
.hero h1 span{ color:var(--text-secondary); font-weight:300; }
.hero p { color:var(--text-secondary); margin-top:12px; font-size:15px; }
.search-row { max-width:520px; margin:18px auto 0; display:flex; gap:10px; }
.search-row .glass-btn{ flex-shrink:0; }
.card { padding:24px; margin-bottom:20px; display:block; transition:transform .25s, box-shadow .25s; position:relative; overflow:hidden; }
.card:hover { transform:translateY(-4px); }
.card-meta { display:flex; gap:12px; align-items:center; font-size:12px; color:var(--text-muted); margin-bottom:12px; }
.card h2 { font-size:22px; margin-bottom:10px; line-height:1.4; }
.card p { color:var(--text-secondary); line-height:1.7; font-size:15px; }
.card-foot { display:flex; justify-content:space-between; align-items:center; margin-top:16px; font-size:13px; color:var(--text-muted); }
.card.clickable { cursor:pointer; }
.pagination { display:flex; gap:8px; justify-content:center; margin-top:24px; }
.page-btn { padding:8px 14px; border-radius:10px; background:var(--glass-bg); border:1px solid var(--glass-border); color:var(--text-secondary); font-size:14px; }
.page-btn.active { background:var(--glass-bg-strong); color:var(--text-primary); }
.article-card { padding:32px; }
.article-card h1 { font-size:30px; line-height:1.4; margin-bottom:14px; }
.article-meta { display:flex; gap:16px; font-size:13px; color:var(--text-muted); margin-bottom:24px; flex-wrap:wrap; }
.article-body { line-height:1.9; font-size:16px; color:var(--text-primary); }
.article-body h2 { font-size:24px; margin:28px 0 14px; }
.article-body h3 { font-size:19px; margin:22px 0 10px; }
.article-body p { margin:14px 0; }
.article-body ul,.article-body ol { margin:14px 0 14px 24px; }
.article-body blockquote { border-left:3px solid var(--text-primary); padding:6px 16px; margin:14px 0; color:var(--text-secondary); font-style:italic; }
.article-body code { background:var(--glass-bg-strong); padding:2px 6px; border-radius:6px; font-size:0.9em; }
.article-body pre { background:rgba(0,0,0,0.05); padding:16px; border-radius:12px; overflow:auto; margin:14px 0; }
[data-theme="dark"] .article-body pre{ background:rgba(255,255,255,0.06);}
.article-body pre code { background:none; padding:0; }
.article-body img { max-width:100%; border-radius:12px; }
.comments { margin-top:24px; }
.comments h3 { margin-bottom:16px; }
.comment-item { padding:16px; margin-bottom:12px; }
.comment-item.reply { margin-left:36px; }
.comment-head { display:flex; gap:10px; align-items:center; font-size:13px; color:var(--text-muted); margin-bottom:8px; }
.comment-body { color:var(--text-secondary); line-height:1.7; font-size:14px; }
.comment-form .field { margin-bottom:14px; }
.login-wrap { max-width:380px; margin:0 auto; padding:40px 0; }
.login-box { padding:32px; }
.login-box h2 { margin-bottom:6px; font-size:22px; }
.login-box .sub { color:var(--text-muted); font-size:13px; margin-bottom:24px; }
.login-page { min-height:100vh; display:flex; align-items:center; justify-content:center; padding:24px; }
.login-page .login-box { width:100%; max-width:380px; padding:38px; }
.login-page .login-brand { text-align:center; font-size:22px; font-weight:800; letter-spacing:1px; margin-bottom:26px; }
.login-page .theme-toggle { position:fixed; top:20px; right:24px; }
.login-page .login-back { text-align:center; margin-top:22px; font-size:13px; color:var(--text-muted); }
.login-page .login-back a { color:var(--text-secondary); }
.login-page .login-back a:hover { color:var(--text-primary); }
.field { margin-bottom:16px; }
.field label { display:block; font-size:13px; color:var(--text-secondary); margin-bottom:6px; }
.alert { padding:12px 16px; border-radius:10px; margin-bottom:16px; font-size:14px; background:var(--glass-bg-strong); border:1px solid var(--glass-border); color:var(--text-primary); }
.alert.error { border-color:rgba(180,60,60,0.4); color:rgba(180,60,60,1); }
[data-theme="dark"] .alert.error{ color:#ff8a8a; border-color:rgba(255,120,120,0.4);}
.page-head { margin-bottom:24px; }
.page-head h1 { font-size:26px; }
.page-head .desc { color:var(--text-muted); font-size:14px; margin-top:6px; }
.about-card { padding:32px; line-height:1.9; }
.about-card h2 { margin:20px 0 10px; }
.admin-head { display:flex; justify-content:space-between; align-items:center; margin-bottom:24px; flex-wrap:wrap; gap:12px; }
.admin-head h1 { font-size:24px; }
.stat-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:16px; margin-bottom:28px; }
.stat-card { padding:20px; text-align:center; }
.stat-card .num { font-size:32px; font-weight:800; }
.stat-card .label { color:var(--text-muted); font-size:13px; margin-top:4px; }
.table-wrap { overflow-x:auto; }
.glass-table { width:100%; border-collapse:collapse; font-size:14px; }
.glass-table th { text-align:left; padding:12px 14px; color:var(--text-muted); font-weight:600; font-size:13px; border-bottom:1px solid var(--glass-border); }
.glass-table td { padding:12px 14px; border-bottom:1px solid var(--glass-border); }
.glass-table tr:hover td { background:var(--glass-bg); }
.tag-badge { display:inline-block; padding:2px 10px; border-radius:999px; font-size:12px; background:var(--glass-bg); border:1px solid var(--glass-border); }
.actions a, .actions button { margin-right:8px; font-size:13px; color:var(--text-secondary); }
.actions a:hover, .actions button:hover{ color:var(--text-primary); }
.form-card { padding:24px; }
.form-card .field label { font-weight:600; }
.form-row { display:flex; gap:12px; }
.form-row > div { flex:1; }
.btn-danger { color:#c0392b; }
[data-theme="dark"] .btn-danger{ color:#ff8a8a;}
.footnote { text-align:center; color:var(--text-muted); font-size:12px; padding:20px 0 0; margin-top:20px; }
.msg { padding:12px 16px; border-radius:10px; margin-bottom:16px; background:var(--glass-bg-strong); border:1px solid var(--glass-border); font-size:14px; }
.side-panel a.nav-link {
display:block; padding:10px 12px; border-radius:8px; margin-bottom:4px;
color:var(--text-secondary); font-size:14px; text-decoration:none; transition:background .2s,color .2s;
}
.side-panel a.nav-link:hover { background:var(--glass-bg-strong); color:var(--text-primary); }
.side-panel a.nav-link.active { background:var(--glass-bg-strong); color:var(--text-primary); font-weight:600; }
.admin-table { width:100%; border-collapse:collapse; background:var(--glass-bg); border:1px solid var(--glass-border); border-radius:12px; overflow:hidden; }
.admin-table th, .admin-table td { padding:12px 14px; text-align:left; border-bottom:1px solid var(--glass-border); font-size:14px; }
.admin-table th { background:var(--glass-bg-strong); font-weight:600; color:var(--text-secondary); white-space:nowrap; }
.admin-table tr:last-child td { border-bottom:none; }
.admin-table tr:hover td { background:var(--glass-bg-strong); }
.admin-table .thumb { width:64px; height:40px; object-fit:cover; border-radius:6px; }
.text-muted { color:var(--text-muted); }
.input, .admin-table select, .admin-table input[type=text], .admin-table input[type=number] {
padding:8px 10px; border-radius:8px; border:1px solid var(--glass-border); background:var(--glass-bg); color:var(--text-primary); font-size:14px;
}
.admin-table input[type=checkbox]{ width:16px; height:16px; }
.pagination { display:flex; gap:8px; align-items:center; margin-top:20px; flex-wrap:wrap; }
.pagination a, .pagination span { padding:6px 12px; border-radius:8px; font-size:13px; border:1px solid var(--glass-border); background:var(--glass-bg); color:var(--text-secondary); }
.pagination a:hover { color:var(--text-primary); }
.pagination .current { background:var(--text-primary); color:var(--bg); font-weight:600; border-color:var(--text-primary); }
.badge-count { display:inline-block; min-width:20px; padding:0 6px; border-radius:999px; background:var(--glass-bg-strong); font-size:12px; text-align:center; }
+43
View File
@@ -0,0 +1,43 @@
(function () {
const btn = document.getElementById("themeBtn");
if (!btn) return;
function applyTheme(t) {
document.documentElement.setAttribute("data-theme", t);
btn.textContent = t === "dark" ? "浅色模式" : "深色模式";
try { localStorage.setItem("glass-theme", t); } catch(e) {}
}
function currentTheme() {
return document.documentElement.getAttribute("data-theme") || "light";
}
var saved = "light";
try { saved = localStorage.getItem("glass-theme") || "light"; } catch(e) {}
applyTheme(saved);
btn.addEventListener("click", function () {
applyTheme(currentTheme() === "dark" ? "light" : "dark");
});
})();
(function () {
var modal = document.getElementById("confirmModal");
if (!modal) return;
var text = document.getElementById("confirmText");
var ok = document.getElementById("confirmOk");
var cancel = document.getElementById("confirmCancel");
var target = null;
function open(msg, href) {
if (text) text.textContent = msg;
target = href;
modal.style.display = "flex";
}
function close() { modal.style.display = "none"; target = null; }
document.querySelectorAll("[data-confirm]").forEach(function (el) {
el.addEventListener("click", function (e) {
e.preventDefault();
open(el.getAttribute("data-confirm"), el.href);
});
});
if (ok) ok.addEventListener("click", function () { if (target) location.href = target; });
if (cancel) cancel.addEventListener("click", close);
modal.addEventListener("click", function (e) { if (e.target === modal) close(); });
document.addEventListener("keydown", function (e) { if (e.key === "Escape") close(); });
})();
+33
View File
@@ -0,0 +1,33 @@
<?php
require_once __DIR__ . "/includes/config.php";
require_once __DIR__ . "/includes/functions.php";
$id = isset($_GET["id"]) ? (int)$_GET["id"] : 0;
$c = db()->prepare("SELECT * FROM categories WHERE id=?"); $c->execute([$id]); $cat = $c->fetch();
if (!$cat) { header("Location: " . SITE_URL . "index.php"); exit; }
$pageName = "category";
$curCategory = $id;
$pageTitle = $cat["name"];
record_visit("category/" . $id);
$perPage = 5;
$st = db()->prepare("SELECT COUNT(*) c FROM articles WHERE category_id=? AND status=?"); $st->execute([$id,"published"]);
$pg = paginate((int)$st->fetch()["c"], isset($_GET["page"])?(int)$_GET["page"]:1, $perPage);
$stmt = db()->prepare("SELECT a.* FROM articles a WHERE a.category_id=? AND a.status=? ORDER BY a.created_at DESC LIMIT ? OFFSET ?");
$stmt->bindValue(1,$id,PDO::PARAM_INT); $stmt->bindValue(2,"published",PDO::PARAM_STR);
$stmt->bindValue(3,$pg["perPage"],PDO::PARAM_INT); $stmt->bindValue(4,$pg["offset"],PDO::PARAM_INT);
$stmt->execute(); $articles = $stmt->fetchAll();
require_once __DIR__ . "/includes/header.php";
?>
<div class="page-head">
<h1><?php echo e($cat["name"]); ?></h1>
<div class="desc">分类下的文章</div>
</div>
<?php if (!$articles) { ?><div class="card glass" style="text-align:center;color:var(--text-muted);padding:60px;">该分类暂无文章</div><?php } ?>
<?php foreach ($articles as $a) { ?>
<article class="card glass clickable" onclick="location.href='<?php echo e(SITE_URL); ?>article.php?id=<?php echo (int)$a["id"]; ?>'">
<div class="card-meta"><span><?php echo e(substr($a["created_at"],0,10)); ?></span><span><?php echo (int)$a["views"]; ?> 次阅读</span></div>
<h2><?php echo e($a["title"]); ?></h2>
<p><?php echo e(mb_strimwidth(strip_tags($a["excerpt"] ?: $a["content"]),0,120,"...")); ?></p>
</article>
<?php } ?>
<?php if ($pg["pages"]>1) { ?><div class="pagination"><?php for($i=1;$i<=$pg["pages"];$i++){ ?><a class="page-btn<?php echo $i===$pg["page"]?" active":""; ?>" href="?id=<?php echo $id; ?>&page=<?php echo $i; ?>"><?php echo $i; ?></a><?php } ?></div><?php } ?>
<?php require_once __DIR__ . "/includes/footer.php"; ?>
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
<?php
if (session_status() === PHP_SESSION_NONE) { session_start(); }
function current_user() {
if (!empty($_SESSION["user_id"])) {
$st = db()->prepare("SELECT * FROM users WHERE id=?");
$st->execute([$_SESSION["user_id"]]);
return $st->fetch();
}
return null;
}
function is_login() {
return !empty($_SESSION["user_id"]);
}
function is_admin() {
$u = current_user();
return $u && $u["role"] === "admin";
}
function require_admin() {
if (!is_admin()) {
header("Location: " . SITE_URL . "login.php");
exit;
}
}
function attempt_login($username, $password) {
$st = db()->prepare("SELECT * FROM users WHERE username=?");
$st->execute([$username]);
$user = $st->fetch();
if ($user && password_verify($password, $user["password_hash"])) {
$_SESSION["user_id"] = $user["id"];
return true;
}
return false;
}
function logout() {
unset($_SESSION["user_id"]);
}
+16
View File
@@ -0,0 +1,16 @@
<?php
define("SITE_NAME", "我的博客");
define("SITE_DESC", "欢迎访问");
define("SITE_URL", "/myblog/");
define("UPLOAD_DIR", __DIR__ . "/../uploads/");
define("DB_HOST", "127.0.0.1");
define("DB_NAME", "myblog");
define("DB_USER", "root");
define("DB_PASS", "root");
define("DB_CHARSET", "utf8mb4");
error_reporting(E_ALL);
ini_set("display_errors", "1");
date_default_timezone_set("Asia/Shanghai");
+19
View File
@@ -0,0 +1,19 @@
<?php
require_once __DIR__ . "/config.php";
function db() {
static $pdo = null;
if ($pdo === null) {
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
try {
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
} catch (PDOException $e) {
die("数据库连接失败: " . htmlspecialchars($e->getMessage()));
}
}
return $pdo;
}
+8
View File
@@ -0,0 +1,8 @@
</main>
<?php include __DIR__ . "/sidebar.php"; ?>
</div>
</div>
<div class="footnote"><?php echo e(SITE_NAME); ?> · 用心记录生活</div>
<script src="<?php echo e(SITE_URL); ?>assets/js/main.js"></script>
</body>
</html>
+62
View File
@@ -0,0 +1,62 @@
<?php
require_once __DIR__ . "/db.php";
function e($str) {
return htmlspecialchars((string)$str, ENT_QUOTES, "UTF-8");
}
function slugify($text) {
$text = strtolower(trim($text));
$text = preg_replace("/[^a-z0-9]+/", "-", $text);
$text = trim($text, "-");
return $text !== "" ? $text : "post-" . uniqid();
}
function get_categories() {
return db()->query("SELECT * FROM categories ORDER BY id")->fetchAll();
}
function get_all_tags() {
return db()->query("SELECT t.*, COUNT(at.article_id) cnt FROM tags t LEFT JOIN article_tags at ON t.id=at.tag_id GROUP BY t.id ORDER BY cnt DESC")->fetchAll();
}
function get_article_tags($articleId) {
$st = db()->prepare("SELECT t.* FROM tags t JOIN article_tags at ON t.id=at.tag_id WHERE at.article_id=?");
$st->execute([$articleId]);
return $st->fetchAll();
}
function get_archives() {
$sql = "SELECT DATE_FORMAT(created_at, ?) ym, COUNT(*) cnt FROM articles WHERE status=? GROUP BY ym ORDER BY ym DESC";
$st = db()->prepare($sql);
$st->execute(["%Y年%m月", "published"]);
return $st->fetchAll();
}
function record_visit($page) {
$ip = $_SERVER["REMOTE_ADDR"] ?? "";
$ua = substr($_SERVER["HTTP_USER_AGENT"] ?? "", 0, 250);
$st = db()->prepare("INSERT IGNORE INTO visitor_stats (visit_date, ip, page, ua) VALUES (CURDATE(), ?, ?, ?)");
$st->execute([$ip, $page, $ua]);
}
function paginate($total, $page, $perPage) {
$pages = max(1, (int)ceil($total / $perPage));
$page = max(1, min($pages, (int)$page));
return [
"page" => $page,
"pages" => $pages,
"offset" => ($page - 1) * $perPage,
"perPage" => $perPage,
];
}
function markdown($text) {
static $parsedown = null;
if ($parsedown === null) {
require_once __DIR__ . "/Parsedown.php";
$parsedown = new Parsedown();
$parsedown->setBreaksEnabled(true);
}
return $parsedown->text((string)$text);
}
+41
View File
@@ -0,0 +1,41 @@
<?php
require_once __DIR__ . "/config.php";
require_once __DIR__ . "/functions.php";
require_once __DIR__ . "/auth.php";
$curPage = isset($pageName) ? $pageName : "";
$navCats = get_categories();
$navTags = get_all_tags();
$navArch = get_archives();
?>
<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo isset($pageTitle) ? e($pageTitle) . " - " . SITE_NAME : SITE_NAME; ?></title>
<meta name="description" content="<?php echo e(SITE_DESC); ?>">
<link rel="stylesheet" href="<?php echo e(SITE_URL); ?>assets/css/style.css">
</head>
<body>
<div class="bg"></div>
<nav class="glass-nav">
<div class="nav-brand"><a href="<?php echo e(SITE_URL); ?>index.php">我的博客</a></div>
<div class="nav-links">
<a class="nav-link<?php echo $curPage==="index"?" active":""; ?>" href="<?php echo e(SITE_URL); ?>index.php">首页</a>
<?php foreach ($navCats as $c) { ?>
<a class="nav-link<?php echo isset($curCategory) && $curCategory==$c["id"]?" active":""; ?>" href="<?php echo e(SITE_URL); ?>category.php?id=<?php echo (int)$c["id"]; ?>"><?php echo e($c["name"]); ?></a>
<?php } ?>
<a class="nav-link<?php echo $curPage==="archive"?" active":""; ?>" href="<?php echo e(SITE_URL); ?>archive.php">归档</a>
<a class="nav-link<?php echo $curPage==="about"?" active":""; ?>" href="<?php echo e(SITE_URL); ?>about.php">关于</a>
<button class="theme-toggle" id="themeBtn">深色模式</button>
<?php if (is_login()) { ?>
<a class="glass-btn primary" href="<?php echo e(SITE_URL); ?>admin/index.php">管理</a>
<a class="glass-btn primary" href="<?php echo e(SITE_URL); ?>logout.php">退出</a>
<?php } else { ?>
<a class="glass-btn primary" href="<?php echo e(SITE_URL); ?>login.php">登录</a>
<?php } ?>
</div>
</nav>
<div class="container">
<div class="layout">
<main>
+26
View File
@@ -0,0 +1,26 @@
<aside class="sidebar">
<div class="side-panel glass">
<h3>热门标签</h3>
<div class="chip-row">
<?php foreach ($navTags as $t) { ?>
<a class="glass-chip" href="<?php echo e(SITE_URL); ?>tag.php?id=<?php echo (int)$t["id"]; ?>"><?php echo e($t["name"]); ?></a>
<?php } ?>
</div>
</div>
<div class="side-panel glass">
<h3>分类</h3>
<div class="chip-row">
<?php foreach ($navCats as $c) { ?>
<a class="glass-chip" href="<?php echo e(SITE_URL); ?>category.php?id=<?php echo (int)$c["id"]; ?>"><?php echo e($c["name"]); ?></a>
<?php } ?>
</div>
</div>
<div class="side-panel glass">
<h3>归档</h3>
<div id="archRow">
<?php foreach ($navArch as $a) { ?>
<div class="arch-item"><a href="<?php echo e(SITE_URL); ?>archive.php?ym=<?php echo e($a["ym"]); ?>"><?php echo e($a["ym"]); ?></a><span><?php echo (int)$a["cnt"]; ?> 篇</span></div>
<?php } ?>
</div>
</div>
</aside>
+42
View File
@@ -0,0 +1,42 @@
<?php
$pageName = "index";
$pageTitle = "";
require_once __DIR__ . "/includes/header.php";
$perPage = 5;
$total = (int)db()->query("SELECT COUNT(*) c FROM articles WHERE status='published'")->fetch()["c"];
$pg = paginate($total, isset($_GET["page"]) ? (int)$_GET["page"] : 1, $perPage);
$stmt = db()->prepare("SELECT a.*, c.name cat_name FROM articles a LEFT JOIN categories c ON a.category_id=c.id WHERE a.status=? ORDER BY a.created_at DESC LIMIT ? OFFSET ?");
$stmt->bindValue(1, "published", PDO::PARAM_STR);
$stmt->bindValue(2, $pg["perPage"], PDO::PARAM_INT);
$stmt->bindValue(3, $pg["offset"], PDO::PARAM_INT);
$stmt->execute();
$articles = $stmt->fetchAll();
?>
<div class="hero">
<h1>我的<span>博客</span></h1>
<p>欢迎访问</p>
<div class="search-row">
<form action="<?php echo e(SITE_URL); ?>search.php" method="get" style="display:flex;gap:10px;width:100%;">
<input class="glass-input" name="q" placeholder="搜索文章..." value="<?php echo e(isset($_GET["q"])?$_GET["q"]:""); ?>">
<button class="glass-btn primary">搜索</button>
</form>
</div>
</div>
<?php if (!$articles) { ?>
<div class="card glass" style="text-align:center;color:var(--text-muted);padding:60px 20px;">暂无已发布文章</div>
<?php } else { foreach ($articles as $a) { ?>
<article class="card glass clickable" onclick="location.href='<?php echo e(SITE_URL); ?>article.php?id=<?php echo (int)$a["id"]; ?>'">
<div class="card-meta"><span class="glass-chip"><?php echo e($a["cat_name"] ?: "未分类"); ?></span><span><?php echo e(substr($a["created_at"],0,10)); ?></span><span><?php echo (int)$a["views"]; ?> 次阅读</span></div>
<h2><?php echo e($a["title"]); ?></h2>
<p><?php echo e(mb_strimwidth(strip_tags($a["excerpt"] ?: $a["content"]), 0, 120, "...")); ?></p>
<div class="card-foot"><span>阅读全文</span></div>
</article>
<?php } } ?>
<?php if ($pg["pages"] > 1) { ?>
<div class="pagination">
<?php for ($i=1; $i<=$pg["pages"]; $i++) { ?>
<a class="page-btn<?php echo $i===$pg["page"]?" active":""; ?>" href="?page=<?php echo $i; ?>"><?php echo $i; ?></a>
<?php } ?>
</div>
<?php } ?>
<?php require_once __DIR__ . "/includes/footer.php"; ?>
+48
View File
@@ -0,0 +1,48 @@
<?php
require_once __DIR__ . "/includes/config.php";
require_once __DIR__ . "/includes/functions.php";
require_once __DIR__ . "/includes/auth.php";
if (is_login()) { header("Location: " . SITE_URL . "index.php"); exit; }
$err = "";
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$username = trim($_POST["username"] ?? "");
$password = $_POST["password"] ?? "";
if ($username === "" || $password === "") {
$err = "请输入用户名和密码";
} elseif (attempt_login($username, $password)) {
header("Location: " . SITE_URL . "admin/index.php");
exit;
} else {
$err = "用户名或密码错误";
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - <?php echo e(SITE_NAME); ?></title>
<meta name="description" content="<?php echo e(SITE_DESC); ?>">
<link rel="stylesheet" href="<?php echo e(SITE_URL); ?>assets/css/style.css">
</head>
<body>
<div class="bg"></div>
<button class="theme-toggle" id="themeBtn" style="position:fixed;top:20px;right:24px;">深色模式</button>
<div class="login-page">
<div class="login-box glass">
<div class="login-brand"><?php echo e(SITE_NAME); ?></div>
<h2 style="text-align:center;">登录</h2>
<div class="sub" style="text-align:center;">登录以管理你的博客</div>
<?php if ($err) { ?><div class="msg err"><?php echo e($err); ?></div><?php } ?>
<form method="post">
<div class="field"><label>用户名</label><input class="glass-input" name="username" value="<?php echo e($_POST["username"] ?? ""); ?>" required></div>
<div class="field"><label>密码</label><input class="glass-input" type="password" name="password" required></div>
<button class="glass-btn primary" style="width:100%;padding:12px;">登 录</button>
</form>
<div class="login-back"><a href="<?php echo e(SITE_URL); ?>index.php">返回首页</a></div>
</div>
</div>
<script src="<?php echo e(SITE_URL); ?>assets/js/main.js"></script>
</body>
</html>
+6
View File
@@ -0,0 +1,6 @@
<?php
require_once __DIR__ . "/includes/config.php";
require_once __DIR__ . "/includes/auth.php";
logout();
header("Location: " . SITE_URL . "login.php");
exit;
+35
View File
@@ -0,0 +1,35 @@
<?php
require_once __DIR__ . "/includes/config.php";
require_once __DIR__ . "/includes/functions.php";
$pageName = "search";
$pageTitle = "搜索";
record_visit("search");
$q = isset($_GET["q"]) ? trim($_GET["q"]) : "";
$articles = [];
if ($q !== "") {
$like = "%" . $q . "%";
$st = db()->prepare("SELECT a.*, c.name cat_name FROM articles a LEFT JOIN categories c ON a.category_id=c.id WHERE a.status=? AND (a.title LIKE ? OR a.content LIKE ? OR a.excerpt LIKE ?) ORDER BY a.created_at DESC");
$st->execute(["published",$like,$like,$like]);
$articles = $st->fetchAll();
}
require_once __DIR__ . "/includes/header.php";
?>
<div class="page-head">
<h1>搜索</h1>
<div class="desc"><?php echo $q !== "" ? "关键词: " . e($q) . " (找到 " . count($articles) . " 篇)" : "输入关键词搜索文章"; ?></div>
</div>
<div class="search-row" style="margin:0 0 24px;">
<form action="<?php echo e(SITE_URL); ?>search.php" method="get" style="display:flex;gap:10px;width:100%;">
<input class="glass-input" name="q" value="<?php echo e($q); ?>" placeholder="搜索文章...">
<button class="glass-btn primary">搜索</button>
</form>
</div>
<?php if ($q !== "" && !$articles) { ?><div class="card glass" style="text-align:center;color:var(--text-muted);padding:60px;">没有找到匹配的文章</div><?php } ?>
<?php foreach ($articles as $a) { ?>
<article class="card glass clickable" onclick="location.href='<?php echo e(SITE_URL); ?>article.php?id=<?php echo (int)$a["id"]; ?>'">
<div class="card-meta"><span class="glass-chip"><?php echo e($a["cat_name"] ?: "未分类"); ?></span><span><?php echo e(substr($a["created_at"],0,10)); ?></span></div>
<h2><?php echo e($a["title"]); ?></h2>
<p><?php echo e(mb_strimwidth(strip_tags($a["excerpt"] ?: $a["content"]),0,140,"...")); ?></p>
</article>
<?php } ?>
<?php require_once __DIR__ . "/includes/footer.php"; ?>
+83
View File
@@ -0,0 +1,83 @@
-- myblog 数据库初始化脚本
-- 7 张表:users, categories, tags, articles, article_tags, comments, visitor_stats
CREATE TABLE IF NOT EXISTS users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
email VARCHAR(120) DEFAULT NULL,
role ENUM('admin','author') NOT NULL DEFAULT 'author',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS categories (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(60) NOT NULL,
slug VARCHAR(80) NOT NULL UNIQUE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS tags (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(60) NOT NULL,
slug VARCHAR(80) NOT NULL UNIQUE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS articles (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
category_id INT UNSIGNED DEFAULT NULL,
title VARCHAR(200) NOT NULL,
slug VARCHAR(220) NOT NULL UNIQUE,
content MEDIUMTEXT NOT NULL,
excerpt VARCHAR(500) DEFAULT NULL,
status ENUM('published','draft') NOT NULL DEFAULT 'draft',
views INT UNSIGNED NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS article_tags (
article_id INT UNSIGNED NOT NULL,
tag_id INT UNSIGNED NOT NULL,
PRIMARY KEY (article_id, tag_id),
FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS comments (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
article_id INT UNSIGNED NOT NULL,
parent_id INT UNSIGNED DEFAULT NULL,
author VARCHAR(60) NOT NULL,
email VARCHAR(120) DEFAULT NULL,
content TEXT NOT NULL,
status ENUM('pending','approved') NOT NULL DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS visitor_stats (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
visit_date DATE NOT NULL,
ip VARCHAR(45) DEFAULT NULL,
page VARCHAR(200) DEFAULT NULL,
ua VARCHAR(255) DEFAULT NULL,
UNIQUE KEY uniq_visit (visit_date, ip, page)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 默认管理员:admin / 123456 (password_hash 加密)
INSERT INTO users (username, password_hash, role) VALUES
('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin')
ON DUPLICATE KEY UPDATE id=id;
-- 示例分类
INSERT INTO categories (name, slug) VALUES
('设计','design'),('前端','frontend'),('后端','backend'),('随笔','notes')
ON DUPLICATE KEY UPDATE id=id;
-- 示例标签
INSERT INTO tags (name, slug) VALUES
('CSS','css'),('PHP','php'),('设计系统','design-system'),('动效','motion')
ON DUPLICATE KEY UPDATE id=id;
+233
View File
@@ -0,0 +1,233 @@
-- MariaDB dump 10.19 Distrib 10.4.32-MariaDB, for Win64 (AMD64)
--
-- Host: localhost Database: myblog
-- ------------------------------------------------------
-- Server version 10.4.32-MariaDB
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `article_tags`
--
DROP TABLE IF EXISTS `article_tags`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `article_tags` (
`article_id` int(10) unsigned NOT NULL,
`tag_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`article_id`,`tag_id`),
KEY `tag_id` (`tag_id`),
CONSTRAINT `article_tags_ibfk_1` FOREIGN KEY (`article_id`) REFERENCES `articles` (`id`) ON DELETE CASCADE,
CONSTRAINT `article_tags_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `article_tags`
--
LOCK TABLES `article_tags` WRITE;
/*!40000 ALTER TABLE `article_tags` DISABLE KEYS */;
INSERT INTO `article_tags` VALUES (2,2),(3,3);
/*!40000 ALTER TABLE `article_tags` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `articles`
--
DROP TABLE IF EXISTS `articles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `articles` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`category_id` int(10) unsigned DEFAULT NULL,
`title` varchar(200) NOT NULL,
`slug` varchar(220) NOT NULL,
`content` mediumtext NOT NULL,
`excerpt` varchar(500) DEFAULT NULL,
`status` enum('published','draft') NOT NULL DEFAULT 'draft',
`views` int(10) unsigned NOT NULL DEFAULT 0,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `slug` (`slug`),
KEY `user_id` (`user_id`),
KEY `category_id` (`category_id`),
KEY `idx_status_created` (`status`,`created_at`),
CONSTRAINT `articles_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
CONSTRAINT `articles_ibfk_2` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `articles`
--
LOCK TABLES `articles` WRITE;
/*!40000 ALTER TABLE `articles` DISABLE KEYS */;
INSERT INTO `articles` VALUES (1,1,4,'TCP 三次握手','tcp','## 什么是 TCP 三次握手\n\nTCP 是面向连接的可靠传输协议。在正式传输数据之前,通信双方需要通过三次握手建立连接,确认彼此的收发能力都正常。\n\n## 三次握手的流程\n\n第一次握手:客户端发送一个带有 SYN 标志的报文,并带上一个初始序列号,请求建立连接。\n\n第二次握手:服务端收到 SYN 后,回复一个带有 SYN 和 ACK 标志的报文,确认收到客户端的请求,同时通知自己的初始序列号。\n\n第三次握手:客户端收到服务端的 SYN+ACK 后,再发送一个 ACK 报文进行确认。此时连接建立,双方可以开始传输数据。\n\n## 为什么要三次握手\n\n两次握手是不够的。如果服务端发出的应答报文丢失,客户端会重发 SYN,可能导致服务端建立多个无效连接,浪费资源。\n\n三次握手可以确保双方都知道\"我能发、你能收\"以及\"你能发、我能收\",从而可靠地建立连接。\n\n## 简单总结\n\n连接建立的过程可以概括为:\n\nSYN -> SYN+ACK -> ACK\n\n三次握手是 TCP 可靠性的基础,它保证了连接的双方在传输数据前都已准备好,让后续的数据传输更加安全可靠。','TCP 通过三次握手建立可靠连接,确保双方就绪后才开始传输数据。','published',72,'2026-09-09 10:13:59','2026-09-09 11:12:19'),(2,1,4,'一杯清茶的时间','post-6aa131674c754','## 一杯清茶的时间\n\n下午三点,泡一杯茶。看茶叶在水中慢慢舒展,热气袅袅升起。\n\n生活的节奏太快,需要这样一杯茶的时间,让自己慢下来,也静下来。\n\n## 茶里的小事\n\n烧水、温杯、投茶、注水,每一个步骤都不急不缓。看着茶叶在热水中一点点舒展开来,仿佛整个下午也跟着慢了下来。茶汤的颜色由浅转深,香气在鼻尖萦绕,这一刻,烦恼似乎都变得遥远。\n\n## 慢一点的智慧\n\n我们习惯了赶时间,却常常忘了,有些事急不得,也催不来。就像这杯茶,火候到了,自然就香。人也一样,给自己一点时间和空间,很多事会自然明朗。\n\n一杯茶,不只是一杯茶,它是一段与自己相处的时光,是喧嚣生活里,为自己留出的一方清净。喝完这一杯,再从容地回到忙碌中去。','生活太快,需要一杯茶让自己慢下来。','published',54,'2026-09-08 10:13:59','2026-09-09 11:06:19'),(3,1,4,'写在周末','post-6aa131674cf54','## 写在周末\n\n周末适合整理,整理房间,也整理心情。\n\n把一周的疲惫卸下,把下周的清单列好,然后从容地迎接新的一周。日子虽平凡,却也值得认真对待。\n\n## 整理生活\n\n周末的早晨,拉开窗帘,让阳光照进来。把散落的物品归位,把积攒了一周的衣服洗好,把桌面的杂物清理干净。当房间变得整洁,心情也会跟着轻盈起来。\n\n## 给自己一点时间\n\n除了整理房间,也别忘了整理心情。翻一翻这周的相册,回想几件开心的小事;或者约上好友,去吃一顿喜欢的饭。生活的意义,往往就藏在这些看似平常的片段里。\n\n周末不是工作的暂停,而是生活的重启。休息好、整理好,才能以更好的状态,去迎接下一个平凡又值得期待的日子。','周末适合整理,整理房间,也整理心情。','published',71,'2026-09-07 10:13:59','2026-09-09 11:06:19');
/*!40000 ALTER TABLE `articles` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `categories`
--
DROP TABLE IF EXISTS `categories`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `categories` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(60) NOT NULL,
`slug` varchar(80) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `slug` (`slug`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `categories`
--
LOCK TABLES `categories` WRITE;
/*!40000 ALTER TABLE `categories` DISABLE KEYS */;
INSERT INTO `categories` VALUES (1,'设计','design'),(2,'前端','frontend'),(3,'后端','backend'),(4,'随笔','notes');
/*!40000 ALTER TABLE `categories` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `comments`
--
DROP TABLE IF EXISTS `comments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `comments` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`article_id` int(10) unsigned NOT NULL,
`parent_id` int(10) unsigned DEFAULT NULL,
`author` varchar(60) NOT NULL,
`email` varchar(120) DEFAULT NULL,
`content` text NOT NULL,
`status` enum('pending','approved') NOT NULL DEFAULT 'pending',
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `article_id` (`article_id`),
CONSTRAINT `comments_ibfk_1` FOREIGN KEY (`article_id`) REFERENCES `articles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `comments`
--
LOCK TABLES `comments` WRITE;
/*!40000 ALTER TABLE `comments` DISABLE KEYS */;
/*!40000 ALTER TABLE `comments` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `tags`
--
DROP TABLE IF EXISTS `tags`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `tags` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(60) NOT NULL,
`slug` varchar(80) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `slug` (`slug`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `tags`
--
LOCK TABLES `tags` WRITE;
/*!40000 ALTER TABLE `tags` DISABLE KEYS */;
INSERT INTO `tags` VALUES (1,'随笔','notes'),(2,'生活','life'),(3,'分享','share');
/*!40000 ALTER TABLE `tags` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `users`
--
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password_hash` varchar(255) NOT NULL,
`email` varchar(120) DEFAULT NULL,
`role` enum('admin','author') NOT NULL DEFAULT 'author',
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `users`
--
LOCK TABLES `users` WRITE;
/*!40000 ALTER TABLE `users` DISABLE KEYS */;
INSERT INTO `users` VALUES (1,'admin','$2y$10$4bocGkDIxtmTyK.S7H5AQ.lm/ZzONQpeuZXHhiuRLERFsZ2iaD0qK',NULL,'admin','2026-09-09 08:07:24');
/*!40000 ALTER TABLE `users` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `visitor_stats`
--
DROP TABLE IF EXISTS `visitor_stats`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `visitor_stats` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`visit_date` date NOT NULL,
`ip` varchar(45) DEFAULT NULL,
`page` varchar(200) DEFAULT NULL,
`ua` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_visit` (`visit_date`,`ip`,`page`)
) ENGINE=InnoDB AUTO_INCREMENT=60 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `visitor_stats`
--
LOCK TABLES `visitor_stats` WRITE;
/*!40000 ALTER TABLE `visitor_stats` DISABLE KEYS */;
INSERT INTO `visitor_stats` VALUES (1,'2026-09-09','::1','article/1','Mozilla/5.0 (Windows NT; Windows NT 10.0; zh-CN) WindowsPowerShell/5.1.26100.8972'),(5,'2026-09-09','::1','article/2','Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36 Edg/152.0.0.0'),(8,'2026-09-09','::1','article/3','Mozilla/5.0 (Windows NT; Windows NT 10.0; zh-CN) WindowsPowerShell/5.1.26100.8972'),(14,'2026-09-09','::1','search','Mozilla/5.0 (Windows NT; Windows NT 10.0; zh-CN) WindowsPowerShell/5.1.26100.8972'),(16,'2026-09-09','::1','archive','Mozilla/5.0 (Windows NT; Windows NT 10.0; zh-CN) WindowsPowerShell/5.1.26100.8972'),(17,'2026-09-09','::1','category/4','Mozilla/5.0 (Windows NT; Windows NT 10.0; zh-CN) WindowsPowerShell/5.1.26100.8972'),(18,'2026-09-09','::1','tag/1','Mozilla/5.0 (Windows NT; Windows NT 10.0; zh-CN) WindowsPowerShell/5.1.26100.8972'),(19,'2026-09-09','::1','about','Mozilla/5.0 (Windows NT; Windows NT 10.0; zh-CN) WindowsPowerShell/5.1.26100.8972'),(37,'2026-09-09','::1','category/1','Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36 Edg/152.0.0.0'),(38,'2026-09-09','::1','category/2','Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36 Edg/152.0.0.0'),(39,'2026-09-09','::1','category/3','Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36 Edg/152.0.0.0');
/*!40000 ALTER TABLE `visitor_stats` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2026-09-09 19:16:36
+31
View File
@@ -0,0 +1,31 @@
<?php
require_once __DIR__ . "/includes/config.php";
require_once __DIR__ . "/includes/functions.php";
$id = isset($_GET["id"]) ? (int)$_GET["id"] : 0;
$t = db()->prepare("SELECT * FROM tags WHERE id=?"); $t->execute([$id]); $tg = $t->fetch();
if (!$tg) { header("Location: " . SITE_URL . "index.php"); exit; }
$pageName = "tag";
$pageTitle = $tg["name"];
record_visit("tag/" . $id);
$perPage = 5;
$st = db()->prepare("SELECT COUNT(*) c FROM article_tags WHERE tag_id=?"); $st->execute([$id]);
$pg = paginate((int)$st->fetch()["c"], isset($_GET["page"])?(int)$_GET["page"]:1, $perPage);
$stmt = db()->prepare("SELECT a.*, c.name cat_name FROM articles a JOIN article_tags at ON a.id=at.article_id LEFT JOIN categories c ON a.category_id=c.id WHERE at.tag_id=? AND a.status=? ORDER BY a.created_at DESC LIMIT ? OFFSET ?");
$stmt->bindValue(1,$id,PDO::PARAM_INT); $stmt->bindValue(2,"published",PDO::PARAM_STR);
$stmt->bindValue(3,$pg["perPage"],PDO::PARAM_INT); $stmt->bindValue(4,$pg["offset"],PDO::PARAM_INT);
$stmt->execute(); $articles = $stmt->fetchAll();
require_once __DIR__ . "/includes/header.php";
?>
<div class="page-head">
<h1>标签: <?php echo e($tg["name"]); ?></h1>
<div class="desc">该标签下的文章</div>
</div>
<?php if (!$articles) { ?><div class="card glass" style="text-align:center;color:var(--text-muted);padding:60px;">该标签暂无文章</div><?php } ?>
<?php foreach ($articles as $a) { ?>
<article class="card glass clickable" onclick="location.href='<?php echo e(SITE_URL); ?>article.php?id=<?php echo (int)$a["id"]; ?>'">
<div class="card-meta"><span class="glass-chip"><?php echo e($a["cat_name"] ?: "未分类"); ?></span><span><?php echo e(substr($a["created_at"],0,10)); ?></span></div>
<h2><?php echo e($a["title"]); ?></h2>
</article>
<?php } ?>
<?php if ($pg["pages"]>1) { ?><div class="pagination"><?php for($i=1;$i<=$pg["pages"];$i++){ ?><a class="page-btn<?php echo $i===$pg["page"]?" active":""; ?>" href="?id=<?php echo $id; ?>&page=<?php echo $i; ?>"><?php echo $i; ?></a><?php } ?></div><?php } ?>
<?php require_once __DIR__ . "/includes/footer.php"; ?>
+68
View File
@@ -0,0 +1,68 @@
# 我的博客 · 环境部署指南
这是一份把本博客部署到另一台 Windows 电脑的说明。跟着步骤做即可。
## 1. 环境要求
- Windows 操作系统
- XAMPP(集成 Apache、PHP、MySQL,下载:https://www.apachefriends.org)
- 需要 PHP 的 pdo_mysql 扩展(XAMPP 默认已启用)
## 2. 安装步骤
### 2.1 安装 XAMPP
下载并安装 XAMPP,安装到任意目录,例如 `C:\xampp`。
### 2.2 放置项目
把整个 `myblog` 文件夹复制到 XAMPP 的 htdocs 目录下:
```
XAMPP安装目录\htdocs\myblog
```
例如:`C:\xampp\htdocs\myblog`
### 2.3 启动服务
1. 打开 XAMPP 控制面板(`xampp-control.exe`)
2. 点击 Apache 的 **Start**
3. 点击 MySQL 的 **Start**
### 2.4 导入数据库
1. 浏览器打开 `http://localhost/phpmyadmin`
2. 在左侧点 **新建**,创建一个名为 `myblog` 的数据库,排序规则选 `utf8mb4_general_ci`
3. 点进 `myblog` 数据库 → 顶部 **导入** → 选择本项目的 `sql/myblog.sql` 文件 → 执行
4. 导入成功后会生成 7 张表,并带有测试文章数据
### 2.5 配置数据库密码
项目的数据库配置在 `includes/config.php`,里面 MySQL 的 root 密码写的是 `root`:
- 如果你的 MySQL root 密码正好是 `root`:**无需改动**
- 如果 root 密码是空(XAMPP 默认):用 phpMyAdmin 把 root 密码设为 `root`,**或**打开 `includes/config.php` 把 `DB_PASS` 改成 `""`(空)
### 2.6 访问网站
- 前台:`http://localhost/myblog/`
- 后台管理:`http://localhost/myblog/admin/`
- 默认管理员账号:用户名 `admin`,密码 `123456`
## 3. 常见问题
| 问题 | 解决方法 |
|------|----------|
| Apache Start 后变红(端口被占用) | 关闭占用 80 端口的程序,或在 httpd.conf 改端口 |
| MySQL Start 后变红(端口被占用) | 关闭占用 3306 端口的程序 |
| 打开页面显示连接数据库失败 | 检查 config.php 的 DB_PASS 是否和 MySQL 实际密码一致 |
| 中文显示乱码 | 确认数据库 myblog 的排序规则是 utf8mb4 |
## 4. 目录说明
- `includes/` 公共函数、数据库连接、认证
- `admin/` 后台管理
- `assets/` CSS / JS
- `sql/` 数据库脚本(myblog.sql 是含数据的完整备份,init.sql 是空表结构)
- `uploads/` 上传目录