init: 我的博客项目
This commit is contained in:
+11
@@ -0,0 +1,11 @@
|
|||||||
|
# 本地上传数据(不提交)
|
||||||
|
uploads/
|
||||||
|
|
||||||
|
# 日志
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# 系统/编辑器文件
|
||||||
|
Thumbs.db
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
@@ -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>
|
||||||
@@ -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"; ?>
|
||||||
@@ -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">
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
@@ -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"; ?>
|
||||||
@@ -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"; ?>
|
||||||
@@ -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"; ?>
|
||||||
@@ -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"; ?>
|
||||||
@@ -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"; ?>
|
||||||
@@ -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
@@ -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
@@ -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"; ?>
|
||||||
@@ -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; }
|
||||||
@@ -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; }
|
||||||
@@ -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(); });
|
||||||
|
})();
|
||||||
@@ -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
@@ -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"]);
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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"; ?>
|
||||||
@@ -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>
|
||||||
@@ -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
@@ -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"; ?>
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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"; ?>
|
||||||
@@ -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/` 上传目录
|
||||||
Reference in New Issue
Block a user