mirror of
https://github.com/zhang-pu/tblog.git
synced 2026-10-02 15:49:29 +08:00
首次上传 tblog 博客项目完整代码
包含后台管理、前端页面和数据库文件
This commit is contained in:
parent
fdeba3dbbf
commit
2a5bdcbc4e
30 changed files with 4233 additions and 0 deletions
365
admin/assets/admin.css
Normal file
365
admin/assets/admin.css
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #f5f7fa;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.admin-container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
background: #1a1a2e;
|
||||
color: #fff;
|
||||
padding: 20px 0;
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
padding: 0 20px 20px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.sidebar nav a {
|
||||
display: block;
|
||||
padding: 12px 20px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
text-decoration: none;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.sidebar nav a:hover,
|
||||
.sidebar nav a.active {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
margin-left: 240px;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
margin-bottom: 30px;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
margin: 30px 0 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 40px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 16px 20px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.badge-draft {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.badge-pending {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.badge-approved {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.badge-rejected {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
background: #22c55e;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: background 0.3s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #16a34a;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #b91c1c;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
background: transparent;
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.btn-link:hover {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="url"],
|
||||
input[type="number"],
|
||||
input[type="password"],
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e5e5e5;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: #22c55e;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 120px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.back-link a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.back-link a:hover {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.status-toggle {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.status-toggle label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #ccc;
|
||||
transition: 0.3s;
|
||||
border-radius: 26px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background: white;
|
||||
transition: 0.3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 60px;
|
||||
}
|
||||
.logo {
|
||||
font-size: 14px;
|
||||
padding: 0 10px 10px;
|
||||
}
|
||||
.sidebar nav a {
|
||||
padding: 12px 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.sidebar nav a span {
|
||||
display: none;
|
||||
}
|
||||
.content {
|
||||
margin-left: 60px;
|
||||
}
|
||||
}
|
||||
169
admin/category.php
Normal file
169
admin/category.php
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
/**
|
||||
* Admin Category Management
|
||||
*/
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once INCLUDE_PATH . 'function.common.php';
|
||||
require_once INCLUDE_PATH . 'function.category.php';
|
||||
|
||||
auth_check();
|
||||
|
||||
$message = '';
|
||||
$edit_category = null;
|
||||
|
||||
// Handle form submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$data = [
|
||||
'name' => trim($_POST['name'] ?? ''),
|
||||
'slug' => trim($_POST['slug'] ?? ''),
|
||||
'description' => trim($_POST['description'] ?? ''),
|
||||
'parent_id' => intval($_POST['parent_id'] ?? 0),
|
||||
'order_num' => intval($_POST['order_num'] ?? 0)
|
||||
];
|
||||
|
||||
if (empty($data['name']) || empty($data['slug'])) {
|
||||
$message = '<div class="error">名称和别名不能为空</div>';
|
||||
} else {
|
||||
if (!empty($_POST['id'])) {
|
||||
update_category(intval($_POST['id']), $data);
|
||||
$message = '<div class="success">分类已更新</div>';
|
||||
} else {
|
||||
create_category($data);
|
||||
$message = '<div class="success">分类已创建</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load category for editing
|
||||
if (!empty($_GET['edit'])) {
|
||||
$edit_category = get_category_by_id(intval($_GET['edit']));
|
||||
}
|
||||
|
||||
// Handle delete
|
||||
if (!empty($_GET['delete'])) {
|
||||
$count = get_category_count(intval($_GET['delete']));
|
||||
if ($count > 0) {
|
||||
$message = '<div class="error">该分类下有 ' . $count . ' 篇文章,无法删除</div>';
|
||||
} else {
|
||||
delete_category(intval($_GET['delete']));
|
||||
$message = '<div class="success">分类已删除</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Get all categories
|
||||
$categories = get_categories();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>分类管理 - ZhangPu Blog</title>
|
||||
<link rel="stylesheet" href="assets/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-container">
|
||||
<aside class="sidebar">
|
||||
<div class="logo">🌿 ZhangPu Blog</div>
|
||||
<nav>
|
||||
<a href="index.php">📊 概览</a>
|
||||
<a href="post.php">📝 文章管理</a>
|
||||
<a href="category.php" class="active">📁 分类管理</a>
|
||||
<a href="comment.php">💬 评论管理</a>
|
||||
<a href="settings.php">⚙️ 设置</a>
|
||||
<a href="logout.php">🚪 退出</a>
|
||||
<a href="../">👁️ 查看博客</a>
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<div class="back-link">
|
||||
<a href="index.php">← 返回概览</a>
|
||||
<a href="?new=1" style="margin-left: 15px;">+ 添加分类</a>
|
||||
</div>
|
||||
|
||||
<?php echo $message; ?>
|
||||
|
||||
<?php if (!empty($_GET['new']) || $edit_category): ?>
|
||||
<h1><?php echo $edit_category ? '编辑分类' : '添加分类'; ?></h1>
|
||||
<form method="POST">
|
||||
<?php if ($edit_category): ?>
|
||||
<input type="hidden" name="id" value="<?php echo $edit_category['id']; ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">名称</label>
|
||||
<input type="text" name="name" id="name" value="<?php echo e($edit_category['name'] ?? ''); ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="slug">别名</label>
|
||||
<input type="text" name="slug" id="slug" value="<?php echo e($edit_category['slug'] ?? ''); ?>" placeholder="如: tech" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">描述</label>
|
||||
<textarea name="description" id="description" rows="3"><?php echo e($edit_category['description'] ?? ''); ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="parent_id">父分类</label>
|
||||
<select name="parent_id" id="parent_id">
|
||||
<option value="0">无</option>
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<?php if (!$edit_category || $cat['id'] != $edit_category['id']): ?>
|
||||
<option value="<?php echo $cat['id']; ?>" <?php echo ($edit_category['parent_id'] ?? 0) == $cat['id'] ? 'selected' : ''; ?>><?php echo e($cat['name']); ?></option>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="order_num">排序</label>
|
||||
<input type="number" name="order_num" id="order_num" value="<?php echo e($edit_category['order_num'] ?? 0); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">保存</button>
|
||||
<a href="category.php" class="btn">取消</a>
|
||||
</div>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<h1>分类管理</h1>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>别名</th>
|
||||
<th>描述</th>
|
||||
<th>文章数</th>
|
||||
<th>排序</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<tr>
|
||||
<td><?php echo e($cat['name']); ?></td>
|
||||
<td><?php echo e($cat['slug']); ?></td>
|
||||
<td><?php echo e($cat['description']); ?></td>
|
||||
<td><?php echo get_category_count($cat['id']); ?></td>
|
||||
<td><?php echo $cat['order_num']; ?></td>
|
||||
<td>
|
||||
<a href="?edit=<?php echo $cat['id']; ?>" class="btn btn-sm">编辑</a>
|
||||
<a href="?delete=<?php echo $cat['id']; ?>" class="btn btn-sm btn-danger" onclick="return confirm('确定要删除吗?')">删除</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($categories)): ?>
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; padding: 40px;">暂无分类</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
158
admin/comment.php
Normal file
158
admin/comment.php
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
<?php
|
||||
/**
|
||||
* Admin Comment Management
|
||||
*/
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once INCLUDE_PATH . 'function.common.php';
|
||||
require_once INCLUDE_PATH . 'function.comment.php';
|
||||
|
||||
auth_check();
|
||||
|
||||
$message = '';
|
||||
|
||||
// Handle status update
|
||||
if (!empty($_POST['action'])) {
|
||||
csrf_verify();
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
if ($id > 0) {
|
||||
if ($_POST['action'] === 'approve') {
|
||||
update_comment_status($id, 'approved');
|
||||
$message = '<div class="success">评论已通过</div>';
|
||||
} elseif ($_POST['action'] === 'reject') {
|
||||
update_comment_status($id, 'rejected');
|
||||
$message = '<div class="success">评论已拒绝</div>';
|
||||
} elseif ($_POST['action'] === 'delete') {
|
||||
delete_comment($id);
|
||||
$message = '<div class="success">评论已删除</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter
|
||||
$status_filter = $_GET['status'] ?? null;
|
||||
$comments = get_all_comments($status_filter);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>评论管理 - ZhangPu Blog</title>
|
||||
<link rel="stylesheet" href="assets/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-container">
|
||||
<aside class="sidebar">
|
||||
<div class="logo">🌿 ZhangPu Blog</div>
|
||||
<nav>
|
||||
<a href="index.php">📊 概览</a>
|
||||
<a href="post.php">📝 文章管理</a>
|
||||
<a href="category.php">📁 分类管理</a>
|
||||
<a href="comment.php" class="active">💬 评论管理</a>
|
||||
<a href="settings.php">⚙️ 设置</a>
|
||||
<a href="logout.php">🚪 退出</a>
|
||||
<a href="../">👁️ 查看博客</a>
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<div class="back-link">
|
||||
<a href="index.php">← 返回概览</a>
|
||||
</div>
|
||||
|
||||
<?php echo $message; ?>
|
||||
|
||||
<h1>评论管理</h1>
|
||||
|
||||
<div class="filter-tabs">
|
||||
<a href="comment.php" class="<?php echo !$status_filter ? 'active' : ''; ?>">全部</a>
|
||||
<a href="comment.php?status=pending" class="<?php echo $status_filter === 'pending' ? 'active' : ''; ?>">待审核</a>
|
||||
<a href="comment.php?status=approved" class="<?php echo $status_filter === 'approved' ? 'active' : ''; ?>">已通过</a>
|
||||
<a href="comment.php?status=rejected" class="<?php echo $status_filter === 'rejected' ? 'active' : ''; ?>">已拒绝</a>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>文章</th>
|
||||
<th>作者</th>
|
||||
<th>内容</th>
|
||||
<th>状态</th>
|
||||
<th>时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($comments as $comment): ?>
|
||||
<tr>
|
||||
<td><a href="../post/<?php echo e($comment['post_slug']); ?>" target="_blank"><?php echo e($comment['post_title']); ?></a></td>
|
||||
<td>
|
||||
<?php echo e($comment['author']); ?>
|
||||
<?php if ($comment['email']): ?>
|
||||
<br><small style="color:#999;"><?php echo e($comment['email']); ?></small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="max-width: 200px;">
|
||||
<div style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"><?php echo e($comment['content']); ?></div>
|
||||
</td>
|
||||
<td><span class="badge badge-<?php echo $comment['status']; ?>"><?php
|
||||
$status_map = ['pending'=>'待审核','approved'=>'已通过','rejected'=>'已拒绝'];
|
||||
echo $status_map[$comment['status']];
|
||||
?></span></td>
|
||||
<td><?php echo date('Y-m-d H:i', strtotime($comment['created_at'])); ?></td>
|
||||
<td>
|
||||
<?php if ($comment['status'] === 'pending'): ?>
|
||||
<form method="POST" style="display:inline;">
|
||||
<?php echo csrf_field(); ?>
|
||||
<input type="hidden" name="id" value="<?php echo $comment['id']; ?>">
|
||||
<input type="hidden" name="action" value="approve">
|
||||
<button type="submit" class="btn btn-sm">通过</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php if ($comment['status'] !== 'rejected'): ?>
|
||||
<form method="POST" style="display:inline;">
|
||||
<?php echo csrf_field(); ?>
|
||||
<input type="hidden" name="id" value="<?php echo $comment['id']; ?>">
|
||||
<input type="hidden" name="action" value="reject">
|
||||
<button type="submit" class="btn btn-sm">拒绝</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<form method="POST" style="display:inline;" onsubmit="return confirm('确定要删除吗?')">
|
||||
<?php echo csrf_field(); ?>
|
||||
<input type="hidden" name="id" value="<?php echo $comment['id']; ?>">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<button type="submit" class="btn btn-sm btn-danger">删除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($comments)): ?>
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; padding: 40px;">暂无评论</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
</div>
|
||||
<style>
|
||||
.filter-tabs {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.filter-tabs a {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
margin-right: 10px;
|
||||
background: #e5e5e5;
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.filter-tabs a:hover,
|
||||
.filter-tabs a.active {
|
||||
background: #22c55e;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
99
admin/index.php
Normal file
99
admin/index.php
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
/**
|
||||
* Admin Dashboard
|
||||
*/
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once INCLUDE_PATH . 'function.common.php';
|
||||
require_once INCLUDE_PATH . 'function.post.php';
|
||||
require_once INCLUDE_PATH . 'function.category.php';
|
||||
require_once INCLUDE_PATH . 'function.comment.php';
|
||||
|
||||
auth_check();
|
||||
|
||||
$total_posts = get_total_posts();
|
||||
$categories = get_categories();
|
||||
$total_comments = get_comment_count();
|
||||
$recent_posts = get_posts(1, 5);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>管理后台 - ZhangPu Blog</title>
|
||||
<link rel="stylesheet" href="assets/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-container">
|
||||
<aside class="sidebar">
|
||||
<div class="logo">🌿 ZhangPu Blog</div>
|
||||
<nav>
|
||||
<a href="index.php" class="active">📊 概览</a>
|
||||
<a href="post.php">📝 文章管理</a>
|
||||
<a href="category.php">📁 分类管理</a>
|
||||
<a href="comment.php">💬 评论管理</a>
|
||||
<a href="settings.php">⚙️ 设置</a>
|
||||
<a href="logout.php">🚪 退出</a>
|
||||
<a href="../">👁️ 查看博客</a>
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<h1>管理后台</h1>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📝</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value"><?php echo $total_posts; ?></div>
|
||||
<div class="stat-label">文章总数</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📁</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value"><?php echo count($categories); ?></div>
|
||||
<div class="stat-label">分类数量</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">💬</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value"><?php echo $total_comments; ?></div>
|
||||
<div class="stat-label">评论总数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>最新文章</h2>
|
||||
<div class="actions">
|
||||
<a href="post.php?new=1" class="btn btn-primary">写新文章</a>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>标题</th>
|
||||
<th>分类</th>
|
||||
<th>状态</th>
|
||||
<th>发布时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($recent_posts as $post): ?>
|
||||
<tr>
|
||||
<td><?php echo e($post['title']); ?></td>
|
||||
<td><?php echo e($post['category_name']); ?></td>
|
||||
<td><span class="badge badge-<?php echo $post['status'] === 'published' ? 'success' : 'draft'; ?>"><?php echo $post['status'] === 'published' ? '已发布' : '草稿'; ?></span></td>
|
||||
<td><?php echo date('Y-m-d', strtotime($post['created_at'])); ?></td>
|
||||
<td>
|
||||
<a href="post.php?edit=<?php echo $post['id']; ?>" class="btn btn-sm">编辑</a>
|
||||
<a href="../post/<?php echo e($post['slug']); ?>" target="_blank" class="btn btn-sm btn-link">查看</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
135
admin/login.php
Normal file
135
admin/login.php
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<?php
|
||||
/**
|
||||
* Admin Login
|
||||
*/
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once INCLUDE_PATH . 'function.common.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
csrf_verify();
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if (auth_verify($username, $password)) {
|
||||
$_SESSION['admin'] = true;
|
||||
$_SESSION['admin_user'] = $username;
|
||||
// Rotate CSRF token on login
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
redirect('index.php');
|
||||
} else {
|
||||
$error = '用户名或密码错误';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 - ZhangPu Blog 管理后台</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #22c55e 0%, #0ea5e9 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.login-box {
|
||||
background: #fff;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
h1 {
|
||||
color: #22c55e;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
font-size: 24px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e5e5e5;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #22c55e;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: #22c55e;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
button:hover {
|
||||
background: #16a34a;
|
||||
}
|
||||
.error {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.back-link {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.back-link a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
}
|
||||
.back-link a:hover {
|
||||
color: #22c55e;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h1>🌿 ZhangPu Blog 管理后台</h1>
|
||||
<?php if (!empty($error)): ?>
|
||||
<div class="error"><?php echo e($error); ?></div>
|
||||
<?php endif; ?>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo csrf_token(); ?>">
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" name="username" id="username" placeholder="请输入用户名" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">管理密码</label>
|
||||
<input type="password" name="password" id="password" placeholder="请输入管理密码" required>
|
||||
</div>
|
||||
<button type="submit">登 录</button>
|
||||
</form>
|
||||
<div class="back-link">
|
||||
<a href="../">← 返回博客</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
9
admin/logout.php
Normal file
9
admin/logout.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
/**
|
||||
* Admin Logout
|
||||
*/
|
||||
session_start();
|
||||
$_SESSION['admin'] = false;
|
||||
session_destroy();
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
379
admin/post.php
Normal file
379
admin/post.php
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
<?php
|
||||
/**
|
||||
* Admin Post Management
|
||||
*/
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once INCLUDE_PATH . 'function.common.php';
|
||||
require_once INCLUDE_PATH . 'function.post.php';
|
||||
require_once INCLUDE_PATH . 'function.category.php';
|
||||
|
||||
auth_check();
|
||||
|
||||
// Pagination
|
||||
$page = max(1, intval($_GET['page'] ?? 1));
|
||||
$per_page = 20;
|
||||
$total = get_total_posts();
|
||||
$total_pages = max(1, ceil($total / $per_page));
|
||||
$all_posts = get_posts($page, $per_page);
|
||||
|
||||
$message = '';
|
||||
$edit_post = null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
csrf_verify();
|
||||
|
||||
$data = [
|
||||
'title' => trim($_POST['title'] ?? ''),
|
||||
'content' => trim($_POST['content'] ?? ''),
|
||||
'excerpt' => trim($_POST['excerpt'] ?? ''),
|
||||
'slug' => trim($_POST['slug'] ?? '') ?: generate_slug($_POST['title'] ?? ''),
|
||||
'category_id' => intval($_POST['category_id'] ?? 1),
|
||||
'template' => $_POST['template'] ?? 'flow',
|
||||
'status' => $_POST['status'] ?? 'published'
|
||||
];
|
||||
|
||||
// Sanitize rich-text content to prevent XSS
|
||||
$data['content'] = sanitize_html($data['content']);
|
||||
|
||||
if (empty($data['title']) || empty($data['content'])) {
|
||||
$message = '<div class="error">标题和内容不能为空</div>';
|
||||
} else {
|
||||
// Check slug uniqueness (only if slug changed or new post)
|
||||
$existing = null;
|
||||
if (!empty($_POST['id'])) {
|
||||
$existing = get_post_by_id(intval($_POST['id']));
|
||||
}
|
||||
$slug_check = $data['slug'];
|
||||
$db = get_db();
|
||||
$stmt = $db->prepare("SELECT id FROM posts WHERE slug = ? AND deleted_at IS NULL AND id != ?");
|
||||
$stmt->bind_param('si', $slug_check, $existing['id'] ?? 0);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
if ($res->num_rows > 0) {
|
||||
$data['slug'] = $data['slug'] . '-' . time();
|
||||
}
|
||||
|
||||
if (!empty($_POST['id'])) {
|
||||
update_post(intval($_POST['id']), $data);
|
||||
$message = '<div class="success">文章已更新</div>';
|
||||
} else {
|
||||
create_post($data);
|
||||
$message = '<div class="success">文章已创建</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($_GET['edit'])) {
|
||||
$edit_post = get_post_by_id(intval($_GET['edit']));
|
||||
}
|
||||
|
||||
if (!empty($_GET['delete'])) {
|
||||
csrf_verify('BOTH');
|
||||
delete_post(intval($_GET['delete']));
|
||||
$message = '<div class="success">文章已删除</div>';
|
||||
}
|
||||
|
||||
$categories = get_categories();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>文章管理 - ZhangPu Blog</title>
|
||||
<link rel="stylesheet" href="assets/admin.css">
|
||||
<style>
|
||||
.editor-wrap { border: 1px solid #ddd; border-radius: 8px; overflow: hidden; margin-bottom: 20px; }
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 2px solid #e5e5e5;
|
||||
background: #fafafa;
|
||||
}
|
||||
.toolbar button {
|
||||
width: 34px; height: 34px;
|
||||
border: 1px solid #e0e0e0;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.toolbar button:hover { background: #22c55e; color: #fff; border-color: #22c55e; }
|
||||
.toolbar .sep { width: 1px; height: 24px; background: #e0e0e0; margin: 5px 4px; }
|
||||
.toolbar select { height: 34px; padding: 0 8px; border: 1px solid #e0e0e0; border-radius: 6px; background: #fff; font-size: 13px; cursor: pointer; }
|
||||
.color-wrap { position: relative; }
|
||||
.color-picker {
|
||||
width: 34px; height: 34px; border: 1px solid #e0e0e0; border-radius: 6px; cursor: pointer; padding: 2px; background: #fff;
|
||||
}
|
||||
.color-picker::-webkit-color-swatch-wrapper { padding: 2px; }
|
||||
.color-picker::-webkit-color-swatch { border: none; border-radius: 4px; }
|
||||
#editor {
|
||||
min-height: 400px;
|
||||
padding: 16px 20px;
|
||||
outline: none;
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
#editor img { max-width: 100%; height: auto; border-radius: 8px; margin: 10px 0; }
|
||||
#editor blockquote { border-left: 4px solid #22c55e; padding-left: 16px; margin: 10px 0; color: #555; }
|
||||
#editor pre { background: #1e1e2e; color: #e0e0e8; padding: 16px; border-radius: 8px; overflow-x: auto; font-family: monospace; }
|
||||
#editor ul, #editor ol { padding-left: 28px; margin: 8px 0; }
|
||||
.footer-bar { padding: 10px 14px; border-top: 1px solid #e5e5e5; background: #fafafa; display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #888; }
|
||||
.footer-bar .word-count span { color: #22c55e; font-weight: bold; }
|
||||
.pagination { margin-top: 20px; display: flex; gap: 4px; align-items: center; }
|
||||
.pagination a, .pagination span { padding: 6px 12px; border: 1px solid #e5e5e5; border-radius: 4px; text-decoration: none; color: #333; }
|
||||
.pagination a:hover { background: #22c55e; color: #fff; border-color: #22c55e; }
|
||||
.pagination .active { background: #22c55e; color: #fff; border-color: #22c55e; }
|
||||
.pagination .disabled { color: #ccc; pointer-events: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-container">
|
||||
<aside class="sidebar">
|
||||
<div class="logo">🌿 ZhangPu Blog</div>
|
||||
<nav>
|
||||
<a href="index.php">📊 概览</a>
|
||||
<a href="post.php" class="active">📝 文章管理</a>
|
||||
<a href="category.php">📁 分类管理</a>
|
||||
<a href="comment.php">💬 评论管理</a>
|
||||
<a href="settings.php">⚙️ 设置</a>
|
||||
<a href="logout.php">🚪 退出</a>
|
||||
<a href="../">👁️ 查看博客</a>
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<div class="back-link">
|
||||
<a href="index.php">← 返回概览</a>
|
||||
<a href="?new=1" style="margin-left: 15px;">+ 写新文章</a>
|
||||
</div>
|
||||
|
||||
<?php echo $message; ?>
|
||||
|
||||
<?php if (!empty($_GET['new']) || $edit_post): ?>
|
||||
<h1><?php echo $edit_post ? '编辑文章' : '写新文章'; ?></h1>
|
||||
<form method="POST" class="post-form" onsubmit="return submitForm()">
|
||||
<?php echo csrf_field(); ?>
|
||||
<?php if ($edit_post): ?>
|
||||
<input type="hidden" name="id" value="<?php echo $edit_post['id']; ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="title">标题</label>
|
||||
<input type="text" name="title" id="title" value="<?php echo e($edit_post['title'] ?? ''); ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="slug">URL别名</label>
|
||||
<input type="text" name="slug" id="slug" value="<?php echo e($edit_post['slug'] ?? ''); ?>" placeholder="auto-generated">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="category_id">分类</label>
|
||||
<select name="category_id" id="category_id">
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<option value="<?php echo $cat['id']; ?>" <?php echo ($edit_post['category_id'] ?? 1) == $cat['id'] ? 'selected' : ''; ?>><?php echo e($cat['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="template">模板</label>
|
||||
<select name="template" id="template">
|
||||
<option value="flow" <?php echo ($edit_post['template'] ?? 'flow') === 'flow' ? 'selected' : ''; ?>>Flow (浅色)</option>
|
||||
<option value="magine" <?php echo ($edit_post['template'] ?? '') === 'magine' ? 'selected' : ''; ?>>Magine (深色)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="excerpt">摘要</label>
|
||||
<textarea name="excerpt" id="excerpt" rows="2"><?php echo e($edit_post['excerpt'] ?? ''); ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>正文</label>
|
||||
<div class="editor-wrap">
|
||||
<div class="toolbar">
|
||||
<select onchange="formatBlock(this.value)" title="标题">
|
||||
<option value="p">正文</option>
|
||||
<option value="h1">标题1</option>
|
||||
<option value="h2">标题2</option>
|
||||
<option value="h3">标题3</option>
|
||||
</select>
|
||||
<div class="sep"></div>
|
||||
<button type="button" onclick="execCmd('bold')" title="加粗"><b>B</b></button>
|
||||
<button type="button" onclick="execCmd('italic')" title="斜体"><i>I</i></button>
|
||||
<button type="button" onclick="execCmd('underline')" title="下划线"><u>U</u></button>
|
||||
<button type="button" onclick="execCmd('strikeThrough')" title="删除线"><s>S</s></button>
|
||||
<div class="sep"></div>
|
||||
<button type="button" onclick="execCmd('insertUnorderedList')" title="无序列表">•</button>
|
||||
<button type="button" onclick="execCmd('insertOrderedList')" title="有序列表">1.</button>
|
||||
<div class="sep"></div>
|
||||
<div class="color-wrap">
|
||||
<input type="color" class="color-picker" value="#333333" onchange="execForeColor(this.value)" title="文字颜色">
|
||||
</div>
|
||||
<div class="sep"></div>
|
||||
<button type="button" onclick="execCmd('formatBlock', 'blockquote')" title="引用">❝</button>
|
||||
<button type="button" onclick="formatBlock('pre')" title="代码块"></></button>
|
||||
<button type="button" onclick="insertLink()" title="链接">🔗</button>
|
||||
<button type="button" onclick="insertImage()" title="图片">🖼️</button>
|
||||
<div class="sep"></div>
|
||||
<button type="button" onclick="execCmd('justifyLeft')" title="左对齐">⬅</button>
|
||||
<button type="button" onclick="execCmd('justifyCenter')" title="居中">⬛</button>
|
||||
<button type="button" onclick="execCmd('justifyRight')" title="右对齐">➡</button>
|
||||
<div class="sep"></div>
|
||||
<button type="button" onclick="execCmd('removeFormat')" title="清除格式">🧹</button>
|
||||
</div>
|
||||
|
||||
<div id="editor" contenteditable="true" oninput="updateCount()"><?php echo $edit_post['content'] ?? ''; ?></div>
|
||||
|
||||
<div class="footer-bar">
|
||||
<span>字数:<span class="word-count"><span id="charCount">0</span> 字</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="content" id="content" value="<?php echo e($edit_post['content'] ?? ''); ?>">
|
||||
</div>
|
||||
|
||||
<div class="status-toggle">
|
||||
<label>
|
||||
<input type="radio" name="status" value="draft" <?php echo ($edit_post['status'] ?? 'published') === 'draft' ? 'checked' : ''; ?>>
|
||||
草稿
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="status" value="published" <?php echo ($edit_post['status'] ?? 'published') === 'published' ? 'checked' : ''; ?>>
|
||||
发布
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">保存</button>
|
||||
<a href="post.php" class="btn">取消</a>
|
||||
</div>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<h1>文章管理 <small style="font-size:14px;color:#888;">(共 <?php echo $total; ?> 篇)</small></h1>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>标题</th>
|
||||
<th>分类</th>
|
||||
<th>模板</th>
|
||||
<th>状态</th>
|
||||
<th>浏览</th>
|
||||
<th>发布时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($all_posts as $post): ?>
|
||||
<tr>
|
||||
<td><?php echo e($post['title']); ?></td>
|
||||
<td><?php echo e($post['category_name']); ?></td>
|
||||
<td><?php echo e($post['template']); ?></td>
|
||||
<td><span class="badge badge-<?php echo $post['status'] === 'published' ? 'success' : 'draft'; ?>"><?php echo $post['status'] === 'published' ? '已发布' : '草稿'; ?></span></td>
|
||||
<td><?php echo $post['views']; ?></td>
|
||||
<td><?php echo date('Y-m-d', strtotime($post['created_at'])); ?></td>
|
||||
<td>
|
||||
<a href="?edit=<?php echo $post['id']; ?>" class="btn btn-sm">编辑</a>
|
||||
<a href="../post/<?php echo e($post['slug']); ?>" target="_blank" class="btn btn-sm btn-link">查看</a>
|
||||
<a href="?delete=<?php echo $post['id']; ?>&csrf_token=<?php echo csrf_token(); ?>" class="btn btn-sm btn-danger" onclick="return confirm('确定要删除吗?')">删除</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($all_posts)): ?>
|
||||
<tr>
|
||||
<td colspan="7" style="text-align: center; padding: 40px;">暂无文章</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php if ($total_pages > 1): ?>
|
||||
<div class="pagination">
|
||||
<?php if ($page > 1): ?>
|
||||
<a href="?page=<?php echo $page - 1; ?>">← 上一页</a>
|
||||
<?php else: ?>
|
||||
<span class="disabled">← 上一页</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$start = max(1, $page - 2);
|
||||
$end = min($total_pages, $page + 2);
|
||||
if ($start > 1) echo '<a href="?page=1">1</a>';
|
||||
if ($start > 2) echo '<span>...</span>';
|
||||
for ($i = $start; $i <= $end; $i++) {
|
||||
if ($i == $page) {
|
||||
echo '<span class="active">' . $i . '</span>';
|
||||
} else {
|
||||
echo '<a href="?page=' . $i . '">' . $i . '</a>';
|
||||
}
|
||||
}
|
||||
if ($end < $total_pages) echo '<span>...</span>';
|
||||
if ($end < $total_pages) echo '<a href="?page=' . $total_pages . '">' . $total_pages . '</a>';
|
||||
?>
|
||||
|
||||
<?php if ($page < $total_pages): ?>
|
||||
<a href="?page=<?php echo $page + 1; ?>">下一页 →</a>
|
||||
<?php else: ?>
|
||||
<span class="disabled">下一页 →</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
</div>
|
||||
<script>
|
||||
const editor = document.getElementById('editor');
|
||||
const contentInput = document.getElementById('content');
|
||||
|
||||
function execCmd(cmd, val) {
|
||||
document.execCommand(cmd, false, val || null);
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
function execForeColor(color) {
|
||||
document.execCommand('foreColor', false, color);
|
||||
}
|
||||
|
||||
function formatBlock(tag) {
|
||||
if (tag === 'blockquote' || tag === 'pre') {
|
||||
execCmd('formatBlock', '<' + tag + '>');
|
||||
} else {
|
||||
execCmd('formatBlock', '<' + tag + '>');
|
||||
}
|
||||
}
|
||||
|
||||
function insertLink() {
|
||||
const url = prompt('输入链接地址:', 'https://');
|
||||
if (url) execCmd('createLink', url);
|
||||
}
|
||||
|
||||
function insertImage() {
|
||||
const url = prompt('输入图片链接地址:', 'https://');
|
||||
if (url) execCmd('insertImage', url);
|
||||
}
|
||||
|
||||
function updateCount() {
|
||||
const text = editor.innerText.replace(/\s/g, '');
|
||||
document.getElementById('charCount').textContent = text.length;
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
contentInput.value = editor.innerHTML;
|
||||
return true;
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.ctrlKey && e.key === 'b') { e.preventDefault(); execCmd('bold'); }
|
||||
if (e.ctrlKey && e.key === 'i') { e.preventDefault(); execCmd('italic'); }
|
||||
if (e.ctrlKey && e.key === 'u') { e.preventDefault(); execCmd('underline'); }
|
||||
});
|
||||
|
||||
updateCount();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
153
admin/settings.php
Normal file
153
admin/settings.php
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
/**
|
||||
* Admin Settings
|
||||
*/
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once INCLUDE_PATH . 'function.common.php';
|
||||
|
||||
auth_check();
|
||||
|
||||
$message = '';
|
||||
|
||||
// Handle form submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
csrf_verify();
|
||||
|
||||
// Admin credentials change
|
||||
if (!empty($_POST['new_username']) || !empty($_POST['new_password'])) {
|
||||
$new_username = trim($_POST['new_username'] ?? '');
|
||||
$new_password = trim($_POST['new_password'] ?? '');
|
||||
$current_pass = $_POST['current_password'] ?? '';
|
||||
|
||||
// Verify current password first
|
||||
if (!auth_verify($_SESSION['admin_user'] ?? '', $current_pass)) {
|
||||
$message = '<div class="error">当前密码错误</div>';
|
||||
} else {
|
||||
if (!empty($new_username)) {
|
||||
update_setting('admin_user', $new_username);
|
||||
}
|
||||
if (!empty($new_password)) {
|
||||
set_admin_password($new_password);
|
||||
}
|
||||
$message = '<div class="success">账号信息已更新(下次登录生效)</div>';
|
||||
}
|
||||
} else {
|
||||
// Site settings
|
||||
update_setting('site_name', trim($_POST['site_name'] ?? ''));
|
||||
update_setting('site_description', trim($_POST['site_description'] ?? ''));
|
||||
update_setting('template', $_POST['template'] ?? 'flow');
|
||||
update_setting('posts_per_page', max(1, intval($_POST['posts_per_page'] ?? 10)));
|
||||
$message = '<div class="success">设置已保存</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Load settings
|
||||
$site_name = get_setting('site_name', '张璞博客');
|
||||
$site_description = get_setting('site_description', '一个简洁优雅的博客');
|
||||
$template = get_setting('template', 'flow');
|
||||
$posts_per_page = get_setting('posts_per_page', '10');
|
||||
$admin_user = get_admin_user();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>设置 - ZhangPu Blog</title>
|
||||
<link rel="stylesheet" href="assets/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-container">
|
||||
<aside class="sidebar">
|
||||
<div class="logo">🌿 ZhangPu Blog</div>
|
||||
<nav>
|
||||
<a href="index.php">📊 概览</a>
|
||||
<a href="post.php">📝 文章管理</a>
|
||||
<a href="category.php">📁 分类管理</a>
|
||||
<a href="comment.php">💬 评论管理</a>
|
||||
<a href="settings.php" class="active">⚙️ 设置</a>
|
||||
<a href="logout.php">🚪 退出</a>
|
||||
<a href="../">👁️ 查看博客</a>
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<div class="back-link">
|
||||
<a href="index.php">← 返回概览</a>
|
||||
</div>
|
||||
|
||||
<?php echo $message; ?>
|
||||
|
||||
<h1>修改登录账号</h1>
|
||||
<form method="POST" class="settings-form" style="margin-bottom: 40px; max-width: 600px;">
|
||||
<?php echo csrf_field(); ?>
|
||||
<div class="form-group">
|
||||
<label for="current_password">当前密码(验证身份)</label>
|
||||
<input type="password" name="current_password" id="current_password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new_username">用户名</label>
|
||||
<input type="text" name="new_username" id="new_username" value="<?php echo e($admin_user); ?>" placeholder="留空则不修改">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new_password">新密码</label>
|
||||
<input type="password" name="new_password" id="new_password" placeholder="留空则不修改,建议8位以上">
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">修改账号</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h1>网站设置</h1>
|
||||
<form method="POST">
|
||||
<?php echo csrf_field(); ?>
|
||||
<div class="form-group">
|
||||
<label for="site_name">网站名称</label>
|
||||
<input type="text" name="site_name" id="site_name" value="<?php echo e($site_name); ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="site_description">网站描述</label>
|
||||
<textarea name="site_description" id="site_description" rows="3"><?php echo e($site_description); ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="template">默认模板</label>
|
||||
<select name="template" id="template">
|
||||
<option value="flow" <?php echo $template === 'flow' ? 'selected' : ''; ?>>Flow (浅色主题)</option>
|
||||
<option value="magine" <?php echo $template === 'magine' ? 'selected' : ''; ?>>Magine (深色主题)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="posts_per_page">每页文章数</label>
|
||||
<input type="number" name="posts_per_page" id="posts_per_page" value="<?php echo e($posts_per_page); ?>" min="1" max="50">
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">保存设置</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h2 style="margin-top: 40px;">系统信息</h2>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td>PHP 版本</td>
|
||||
<td><?php echo PHP_VERSION; ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MySQL 客户端版本</td>
|
||||
<td><?php echo mysqli_get_client_info(); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>服务器软件</td>
|
||||
<td><?php echo $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown'; ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>当前登录用户</td>
|
||||
<td><?php echo e($admin_user); ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
121
include/HTMLPurifier.simple.php
Normal file
121
include/HTMLPurifier.simple.php
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* Lightweight HTML sanitizer using DOMDocument + blacklist
|
||||
* For production use, consider installing HTMLPurifier via Composer.
|
||||
*/
|
||||
function sanitize_html($html) {
|
||||
if (empty($html)) return '';
|
||||
|
||||
// Step 1: Remove script, style, and dangerous tags via regex pre-clean
|
||||
$html = preg_replace('#<(script|style|iframe|object|embed|form|input|button)[^>]*>.*?</\1>#is', '', $html);
|
||||
$html = preg_replace('/<[^>]+\s+on\w+\s*=\s*["\'][^"\']*["\']/i', '', $html); // remove on* attributes
|
||||
$html = preg_replace('/<[^>]+\s+style\s*=\s*["\'][^"\']*["\']/i', '', $html); // remove style attrs
|
||||
|
||||
// Step 2: Use DOMDocument to re-serialize (normalizes malformed HTML)
|
||||
libxml_use_internal_errors(true);
|
||||
$dom = new DOMDocument();
|
||||
$dom->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">' . "\n" . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
libxml_clear_errors();
|
||||
|
||||
// Step 3: Walk all elements and enforce whitelist
|
||||
$allowed_tags = [
|
||||
'h1','h2','h3','h4','h5','h6',
|
||||
'p','br','strong','em','u','s','del','sup','sub',
|
||||
'blockquote','pre','code',
|
||||
'ul','ol','li',
|
||||
'a','img',
|
||||
'table','thead','tbody','tr','th','td',
|
||||
'hr','span',
|
||||
'div','section','article','header','footer',
|
||||
];
|
||||
$allowed_attrs = [
|
||||
'href', 'target', 'src', 'alt', 'width', 'height', 'class', 'id',
|
||||
];
|
||||
|
||||
$xpath = new DOMXPath($dom);
|
||||
$all_elements = $xpath->query('//*');
|
||||
$to_remove = [];
|
||||
|
||||
foreach ($all_elements as $el) {
|
||||
$tag = strtolower($el->nodeName);
|
||||
// Remove disallowed tags entirely
|
||||
if (!in_array($tag, $allowed_tags)) {
|
||||
// Keep children, remove tag wrapper
|
||||
foreach ($el->childNodes as $child) {
|
||||
$el->parentNode->insertBefore($child->cloneNode(true), $el);
|
||||
}
|
||||
$to_remove[] = $el;
|
||||
continue;
|
||||
}
|
||||
// Remove disallowed attributes
|
||||
$attrs_to_remove = [];
|
||||
if ($el->attributes) {
|
||||
foreach ($el->attributes as $attr) {
|
||||
if (!in_array(strtolower($attr->nodeName), $allowed_attrs)) {
|
||||
$attrs_to_remove[] = $attr;
|
||||
}
|
||||
}
|
||||
foreach ($attrs_to_remove as $attr) {
|
||||
$el->removeAttributeNode($attr);
|
||||
}
|
||||
}
|
||||
// Sanitize href/src URLs (allow only http/https/data)
|
||||
if ($el->hasAttribute('href')) {
|
||||
$href = $el->getAttribute('href');
|
||||
if (!preg_match('#^https?://|mailto:|tel:|data:#i', $href)) {
|
||||
$el->removeAttribute('href');
|
||||
}
|
||||
}
|
||||
if ($el->hasAttribute('src')) {
|
||||
$src = $el->getAttribute('src');
|
||||
if (!preg_match('#^https?://|data:#i', $src)) {
|
||||
$el->removeAttribute('src');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($to_remove as $el) {
|
||||
$el->parentNode->removeChild($el);
|
||||
}
|
||||
|
||||
$body = $dom->getElementsByTagName('body')->item(0);
|
||||
$output = '';
|
||||
if ($body) {
|
||||
foreach ($body->childNodes as $child) {
|
||||
$output .= $dom->saveHTML($child);
|
||||
}
|
||||
}
|
||||
|
||||
return trim($output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-detect base URL from request
|
||||
*/
|
||||
function get_site_url() {
|
||||
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
return $scheme . '://' . $host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiting for comment submissions
|
||||
*/
|
||||
function rate_limit($key, $max_requests = 5, $window_seconds = 60) {
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$cache_file = '/tmp/rate_limit_' . md5($key . '_' . $ip) . '.json';
|
||||
$now = time();
|
||||
$data = [];
|
||||
if (file_exists($cache_file)) {
|
||||
$data = json_decode(file_get_contents($cache_file), true) ?: [];
|
||||
}
|
||||
// Remove expired entries
|
||||
$data = array_filter($data, function($ts) use ($now, $window_seconds) {
|
||||
return ($now - $ts) < $window_seconds;
|
||||
});
|
||||
if (count($data) >= $max_requests) {
|
||||
return false;
|
||||
}
|
||||
$data[] = $now;
|
||||
file_put_contents($cache_file, json_encode($data));
|
||||
return true;
|
||||
}
|
||||
16
include/db.php
Normal file
16
include/db.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
/**
|
||||
* Database Connection
|
||||
*/
|
||||
|
||||
function get_db() {
|
||||
static $db = null;
|
||||
if ($db === null) {
|
||||
$db = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT);
|
||||
if ($db->connect_error) {
|
||||
die('数据库连接失败: ' . $db->connect_error);
|
||||
}
|
||||
$db->set_charset('utf8mb4');
|
||||
}
|
||||
return $db;
|
||||
}
|
||||
56
include/function.category.php
Normal file
56
include/function.category.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
/**
|
||||
* Category Functions
|
||||
*/
|
||||
|
||||
function get_categories() {
|
||||
$db = get_db();
|
||||
$result = $db->query("SELECT * FROM categories ORDER BY order_num ASC, id ASC");
|
||||
return $result->fetch_all(MYSQLI_ASSOC);
|
||||
}
|
||||
|
||||
function get_category_by_slug($slug) {
|
||||
$db = get_db();
|
||||
$stmt = $db->prepare("SELECT * FROM categories WHERE slug = ?");
|
||||
$stmt->bind_param('s', $slug);
|
||||
$stmt->execute();
|
||||
return $stmt->get_result()->fetch_assoc();
|
||||
}
|
||||
|
||||
function get_category_by_id($id) {
|
||||
$db = get_db();
|
||||
$stmt = $db->prepare("SELECT * FROM categories WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
return $stmt->get_result()->fetch_assoc();
|
||||
}
|
||||
|
||||
function create_category($data) {
|
||||
$db = get_db();
|
||||
$stmt = $db->prepare("INSERT INTO categories (name, slug, description, parent_id, order_num) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->bind_param('sssii', $data['name'], $data['slug'], $data['description'], $data['parent_id'], $data['order_num']);
|
||||
return $stmt->execute();
|
||||
}
|
||||
|
||||
function update_category($id, $data) {
|
||||
$db = get_db();
|
||||
$stmt = $db->prepare("UPDATE categories SET name = ?, slug = ?, description = ?, parent_id = ?, order_num = ? WHERE id = ?");
|
||||
$stmt->bind_param('sssiii', $data['name'], $data['slug'], $data['description'], $data['parent_id'], $data['order_num'], $id);
|
||||
return $stmt->execute();
|
||||
}
|
||||
|
||||
function delete_category($id) {
|
||||
$db = get_db();
|
||||
$stmt = $db->prepare("DELETE FROM categories WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
return $stmt->execute();
|
||||
}
|
||||
|
||||
function get_category_count($category_id) {
|
||||
$db = get_db();
|
||||
$stmt = $db->prepare("SELECT COUNT(*) FROM posts WHERE category_id = ? AND status = 'published' AND deleted_at IS NULL");
|
||||
$stmt->bind_param('i', $category_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
return $result->fetch_row()[0];
|
||||
}
|
||||
103
include/function.comment.php
Normal file
103
include/function.comment.php
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
/**
|
||||
* Comment Functions
|
||||
*/
|
||||
|
||||
function get_comments($post_id) {
|
||||
$db = get_db();
|
||||
$stmt = $db->prepare("SELECT * FROM comments WHERE post_id = ? AND status = 'approved' ORDER BY created_at ASC");
|
||||
$stmt->bind_param('i', $post_id);
|
||||
$stmt->execute();
|
||||
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
}
|
||||
|
||||
function get_all_comments($status = null) {
|
||||
$db = get_db();
|
||||
if ($status) {
|
||||
$stmt = $db->prepare("SELECT c.*, p.title as post_title, p.slug as post_slug
|
||||
FROM comments c
|
||||
LEFT JOIN posts p ON c.post_id = p.id
|
||||
WHERE c.status = ?
|
||||
ORDER BY c.created_at DESC");
|
||||
$stmt->bind_param('s', $status);
|
||||
$stmt->execute();
|
||||
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
} else {
|
||||
$result = $db->query("SELECT c.*, p.title as post_title, p.slug as post_slug
|
||||
FROM comments c
|
||||
LEFT JOIN posts p ON c.post_id = p.id
|
||||
ORDER BY c.created_at DESC");
|
||||
return $result->fetch_all(MYSQLI_ASSOC);
|
||||
}
|
||||
}
|
||||
|
||||
// Spam keywords (case-insensitive)
|
||||
function is_spam_content($content, $author = '', $url = '') {
|
||||
$spam_keywords = [
|
||||
'casino', 'viagra', 'cialis', 'loan', 'mortgage', 'bitcoin',
|
||||
'cheap jerseys', 'nfl jerseys', 'michael kors outlet',
|
||||
'louis vuitton', 'ugg boots', 'pandora', 'tiffany',
|
||||
'viagra', 'cialis', 'levitra', 'cialis',
|
||||
'adult', 'porn', 'nude',
|
||||
'essay', 'dissertation', 'thesis',
|
||||
'backlink', 'sexxx', 'fuck',
|
||||
];
|
||||
|
||||
$check = strtolower($author . ' ' . $content . ' ' . $url);
|
||||
foreach ($spam_keywords as $keyword) {
|
||||
if (strpos($check, strtolower($keyword)) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function create_comment($data) {
|
||||
// Rate limiting: max 5 comments per minute per IP
|
||||
if (!rate_limit('comment', 5, 60)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Honeypot check
|
||||
if (!empty($data['website'])) {
|
||||
return false; // Bot detected
|
||||
}
|
||||
|
||||
// Keyword spam check
|
||||
if (is_spam_content($data['content'], $data['author'], $data['url'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$db = get_db();
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
$stmt = $db->prepare("INSERT INTO comments (post_id, author, email, url, content, ip, status) VALUES (?, ?, ?, ?, ?, ?, 'pending')");
|
||||
$stmt->bind_param('isssss', $data['post_id'], $data['author'], $data['email'], $data['url'], $data['content'], $ip);
|
||||
return $stmt->execute();
|
||||
}
|
||||
|
||||
function update_comment_status($id, $status) {
|
||||
$db = get_db();
|
||||
$stmt = $db->prepare("UPDATE comments SET status = ? WHERE id = ?");
|
||||
$stmt->bind_param('si', $status, $id);
|
||||
return $stmt->execute();
|
||||
}
|
||||
|
||||
function delete_comment($id) {
|
||||
$db = get_db();
|
||||
$stmt = $db->prepare("DELETE FROM comments WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
return $stmt->execute();
|
||||
}
|
||||
|
||||
function get_comment_count($status = null) {
|
||||
$db = get_db();
|
||||
if ($status) {
|
||||
$stmt = $db->prepare("SELECT COUNT(*) FROM comments WHERE status = ?");
|
||||
$stmt->bind_param('s', $status);
|
||||
$stmt->execute();
|
||||
return $stmt->get_result()->fetch_row()[0];
|
||||
} else {
|
||||
$result = $db->query("SELECT COUNT(*) FROM comments");
|
||||
return $result->fetch_row()[0];
|
||||
}
|
||||
}
|
||||
251
include/function.common.php
Normal file
251
include/function.common.php
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
<?php
|
||||
/**
|
||||
* Common Functions
|
||||
*/
|
||||
|
||||
function get_db() {
|
||||
static $db = null;
|
||||
if ($db === null) {
|
||||
$db = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT);
|
||||
if ($db->connect_error) {
|
||||
die('数据库连接失败: ' . $db->connect_error);
|
||||
}
|
||||
$db->set_charset('utf8mb4');
|
||||
}
|
||||
return $db;
|
||||
}
|
||||
|
||||
function e($str) {
|
||||
return htmlspecialchars($str ?? '', ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function redirect($url) {
|
||||
header('Location: ' . $url);
|
||||
exit;
|
||||
}
|
||||
|
||||
function json_response($data, $code = 200) {
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSRF Protection
|
||||
*/
|
||||
function csrf_token() {
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return $_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
function csrf_field() {
|
||||
return '<input type="hidden" name="csrf_token" value="' . csrf_token() . '">';
|
||||
}
|
||||
|
||||
function csrf_verify($method = 'POST') {
|
||||
if ($method === 'POST' || $method === 'BOTH') {
|
||||
$token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
if (empty($token) || !hash_equals($_SESSION['csrf_token'] ?? '', $token)) {
|
||||
http_response_code(403);
|
||||
echo 'CSRF token mismatch';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication
|
||||
*/
|
||||
function get_admin_user() {
|
||||
return get_setting('admin_user', 'admin');
|
||||
}
|
||||
|
||||
function get_admin_hash() {
|
||||
return get_setting('admin_hash', '');
|
||||
}
|
||||
|
||||
function verify_admin_password($password) {
|
||||
$hash = get_admin_hash();
|
||||
if (empty($hash)) {
|
||||
// Fallback: no hash set yet, use legacy plain-text check
|
||||
// After first settings save this branch is never reached
|
||||
return false;
|
||||
}
|
||||
return password_verify($password, $hash);
|
||||
}
|
||||
|
||||
function set_admin_password($password) {
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
update_setting('admin_hash', $hash);
|
||||
}
|
||||
|
||||
function auth_check() {
|
||||
if (empty($_SESSION['admin'])) {
|
||||
redirect('login.php');
|
||||
}
|
||||
}
|
||||
|
||||
function auth_verify($username, $password) {
|
||||
if ($username !== get_admin_user()) {
|
||||
return false;
|
||||
}
|
||||
return verify_admin_password($password);
|
||||
}
|
||||
|
||||
function get_setting($key, $default = '') {
|
||||
$db = get_db();
|
||||
$stmt = $db->prepare("SELECT value FROM settings WHERE `key` = ?");
|
||||
$stmt->bind_param('s', $key);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
if ($row = $result->fetch_assoc()) {
|
||||
return $row['value'];
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
|
||||
function update_setting($key, $value) {
|
||||
$db = get_db();
|
||||
$stmt = $db->prepare("INSERT INTO settings (`key`, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value)");
|
||||
$stmt->bind_param('ss', $key, $value);
|
||||
return $stmt->execute();
|
||||
}
|
||||
|
||||
function time_ago($datetime) {
|
||||
$timestamp = strtotime($datetime);
|
||||
$diff = time() - $timestamp;
|
||||
if ($diff < 60) return '刚刚';
|
||||
if ($diff < 3600) return floor($diff / 60) . '分钟前';
|
||||
if ($diff < 86400) return floor($diff / 3600) . '小时前';
|
||||
if ($diff < 2592000) return floor($diff / 86400) . '天前';
|
||||
return date('Y-m-d', $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML sanitizer - prevent XSS in rich-text content
|
||||
* Uses DOMDocument + whitelist approach (no external dependencies needed)
|
||||
*/
|
||||
function sanitize_html($html) {
|
||||
if (empty($html)) return '';
|
||||
|
||||
// Step 1: Remove dangerous tags via regex pre-clean (before DOM parsing)
|
||||
$html = preg_replace('#<(script|style|iframe|object|embed|form|input|button|select|textarea)[^>]*>.*?</\1>#is', '', $html);
|
||||
|
||||
// Step 2: Remove event-handler attributes (onclick, onerror, etc.)
|
||||
$html = preg_replace('/\s+on\w+\s*=\s*["\'][^"\']*["\']/i', '', $html);
|
||||
|
||||
// Step 3: Use DOMDocument to normalize and re-serialize
|
||||
libxml_use_internal_errors(true);
|
||||
$dom = new DOMDocument();
|
||||
// Wrap in a div so we can use loadHTML on fragment
|
||||
$dom->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">' . "\n" . '<div>' . $html . '</div>', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
libxml_clear_errors();
|
||||
|
||||
// Whitelist of allowed tags and attributes
|
||||
$allowed_tags = [
|
||||
'h1','h2','h3','h4','h5','h6',
|
||||
'p','br','hr',
|
||||
'strong','b','em','i','u','s','del','sup','sub','mark',
|
||||
'blockquote','pre','code',
|
||||
'ul','ol','li',
|
||||
'a','img',
|
||||
'table','thead','tbody','tfoot','tr','th','td',
|
||||
'div','span','section','article','header','footer','aside',
|
||||
];
|
||||
$allowed_attrs = [
|
||||
'href', 'src', 'alt', 'title', 'width', 'height',
|
||||
'class', 'id', 'style',
|
||||
];
|
||||
|
||||
$xpath = new DOMXPath($dom);
|
||||
$all = $xpath->query('//*');
|
||||
$remove = [];
|
||||
|
||||
foreach ($all as $el) {
|
||||
$tag = strtolower($el->nodeName);
|
||||
if (!in_array($tag, $allowed_tags)) {
|
||||
// Replace tag with its children
|
||||
foreach ($el->childNodes as $child) {
|
||||
$el->parentNode->insertBefore($child->cloneNode(true), $el);
|
||||
}
|
||||
$remove[] = $el;
|
||||
continue;
|
||||
}
|
||||
// Filter attributes
|
||||
$bad = [];
|
||||
if ($el->attributes) {
|
||||
foreach ($el->attributes as $attr) {
|
||||
$name = strtolower($attr->nodeName);
|
||||
if (!in_array($name, $allowed_attrs)) {
|
||||
$bad[] = $attr;
|
||||
continue;
|
||||
}
|
||||
// Sanitize URL attributes
|
||||
if (in_array($name, ['href', 'src'])) {
|
||||
$val = $attr->nodeValue;
|
||||
if (!preg_match('#^(https?://|mailto:|tel:|data:)#i', $val)) {
|
||||
// Remove javascript: and other dangerous protocols
|
||||
if (preg_match('#^javascript:#i', $val)) {
|
||||
$bad[] = $attr;
|
||||
continue;
|
||||
}
|
||||
// For src/href, if it doesn't match safe schemes, clear it
|
||||
if (!preg_match('#^https?://|data:#i', $val)) {
|
||||
$bad[] = $attr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($bad as $attr) {
|
||||
$el->removeAttributeNode($attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($remove as $el) {
|
||||
$el->parentNode->removeChild($el);
|
||||
}
|
||||
|
||||
// Extract content from the wrapper div
|
||||
$out = '';
|
||||
foreach ($dom->getElementsByTagName('div')->item(0)->childNodes as $node) {
|
||||
$out .= $dom->saveHTML($node);
|
||||
}
|
||||
return trim($out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiting for comment submissions
|
||||
*/
|
||||
function rate_limit($key, $max_requests = 5, $window_seconds = 60) {
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$cache_file = '/tmp/rate_limit_' . md5($key . '_' . $ip) . '.json';
|
||||
$now = time();
|
||||
$data = [];
|
||||
if (file_exists($cache_file)) {
|
||||
$data = json_decode(file_get_contents($cache_file), true) ?: [];
|
||||
}
|
||||
$data = array_values(array_filter($data, function($ts) use ($now, $window_seconds) {
|
||||
return ($now - $ts) < $window_seconds;
|
||||
}));
|
||||
if (count($data) >= $max_requests) {
|
||||
return false;
|
||||
}
|
||||
$data[] = $now;
|
||||
@file_put_contents($cache_file, json_encode($data));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-detect site URL from current request
|
||||
*/
|
||||
function get_site_url() {
|
||||
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
$base = $scheme . '://' . $host;
|
||||
$script = dirname($_SERVER['SCRIPT_NAME'] ?? '');
|
||||
return rtrim($base . ($script === '/' || $script === '\\' ? '' : $script), '/');
|
||||
}
|
||||
139
include/function.post.php
Normal file
139
include/function.post.php
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
/**
|
||||
* Post Functions
|
||||
*/
|
||||
|
||||
function get_posts($page = 1, $per_page = 10, $category_id = null) {
|
||||
$db = get_db();
|
||||
$offset = ($page - 1) * $per_page;
|
||||
|
||||
$where = "WHERE p.status = 'published' AND p.deleted_at IS NULL";
|
||||
$params = [];
|
||||
$types = '';
|
||||
|
||||
if ($category_id) {
|
||||
$where .= " AND p.category_id = ?";
|
||||
$params[] = $category_id;
|
||||
$types .= 'i';
|
||||
}
|
||||
|
||||
$sql = "SELECT p.*, c.name as category_name, c.slug as category_slug
|
||||
FROM posts p
|
||||
LEFT JOIN categories c ON p.category_id = c.id
|
||||
$where
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT ? OFFSET ?";
|
||||
$params[] = $per_page;
|
||||
$params[] = $offset;
|
||||
$types .= 'ii';
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
if ($params) {
|
||||
$stmt->bind_param($types, ...$params);
|
||||
}
|
||||
$stmt->execute();
|
||||
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
}
|
||||
|
||||
function get_total_posts($category_id = null) {
|
||||
$db = get_db();
|
||||
$where = "WHERE status = 'published' AND deleted_at IS NULL";
|
||||
$params = [];
|
||||
$types = '';
|
||||
|
||||
if ($category_id) {
|
||||
$where .= " AND category_id = ?";
|
||||
$params[] = $category_id;
|
||||
$types .= 'i';
|
||||
}
|
||||
|
||||
$sql = "SELECT COUNT(*) FROM posts $where";
|
||||
$stmt = $db->prepare($sql);
|
||||
if ($params) {
|
||||
$stmt->bind_param($types, ...$params);
|
||||
}
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
return $result->fetch_row()[0];
|
||||
}
|
||||
|
||||
function get_post_by_slug($slug) {
|
||||
$db = get_db();
|
||||
$stmt = $db->prepare("SELECT p.*, c.name as category_name, c.slug as category_slug
|
||||
FROM posts p
|
||||
LEFT JOIN categories c ON p.category_id = c.id
|
||||
WHERE p.slug = ? AND p.status = 'published' AND p.deleted_at IS NULL");
|
||||
$stmt->bind_param('s', $slug);
|
||||
$stmt->execute();
|
||||
$post = $stmt->get_result()->fetch_assoc();
|
||||
|
||||
if ($post) {
|
||||
$stmt2 = $db->prepare("UPDATE posts SET views = views + 1 WHERE id = ?");
|
||||
$stmt2->bind_param('i', $post['id']);
|
||||
$stmt2->execute();
|
||||
}
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
function get_post_by_id($id) {
|
||||
$db = get_db();
|
||||
$stmt = $db->prepare("SELECT * FROM posts WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
return $stmt->get_result()->fetch_assoc();
|
||||
}
|
||||
|
||||
function create_post($data) {
|
||||
$db = get_db();
|
||||
$stmt = $db->prepare("INSERT INTO posts (title, content, excerpt, slug, category_id, template, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->bind_param('sssssss',
|
||||
$data['title'],
|
||||
$data['content'],
|
||||
$data['excerpt'],
|
||||
$data['slug'],
|
||||
$data['category_id'],
|
||||
$data['template'],
|
||||
$data['status']
|
||||
);
|
||||
return $stmt->execute();
|
||||
}
|
||||
|
||||
function update_post($id, $data) {
|
||||
$db = get_db();
|
||||
$stmt = $db->prepare("UPDATE posts SET title = ?, content = ?, excerpt = ?, slug = ?, category_id = ?, template = ?, status = ?, updated_at = NOW() WHERE id = ?");
|
||||
$stmt->bind_param('sssssssi',
|
||||
$data['title'],
|
||||
$data['content'],
|
||||
$data['excerpt'],
|
||||
$data['slug'],
|
||||
$data['category_id'],
|
||||
$data['template'],
|
||||
$data['status'],
|
||||
$id
|
||||
);
|
||||
return $stmt->execute();
|
||||
}
|
||||
|
||||
function delete_post($id) {
|
||||
$db = get_db();
|
||||
$stmt = $db->prepare("UPDATE posts SET deleted_at = NOW() WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
return $stmt->execute();
|
||||
}
|
||||
|
||||
function generate_slug($title) {
|
||||
$slug = preg_replace('/[^\x{4e00}-\x{9fa5}a-zA-Z0-9]+/u', '-', $title);
|
||||
$slug = trim($slug, '-');
|
||||
$slug = strtolower($slug);
|
||||
return $slug ?: 'post-' . time();
|
||||
}
|
||||
|
||||
function get_recent_posts($limit = 5) {
|
||||
$db = get_db();
|
||||
$stmt = $db->prepare("SELECT id, title, slug, created_at FROM posts WHERE status = 'published' AND deleted_at IS NULL ORDER BY created_at DESC LIMIT ?");
|
||||
$stmt->bind_param('i', $limit);
|
||||
$stmt->execute();
|
||||
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
}
|
||||
70
include/router.php
Normal file
70
include/router.php
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
/**
|
||||
* URL Router
|
||||
*/
|
||||
|
||||
function route_request($route) {
|
||||
$route = trim($route, '/');
|
||||
|
||||
if (empty($route) || $route === 'index.php') {
|
||||
include TEMPLATE_PATH . 'flow/index.php';
|
||||
return;
|
||||
}
|
||||
|
||||
// Admin routes
|
||||
if (strpos($route, 'admin/') === 0) {
|
||||
$admin_route = substr($route, 6);
|
||||
|
||||
if (empty($admin_route) || $admin_route === 'index.php') {
|
||||
include ROOT_PATH . 'admin/index.php';
|
||||
} elseif ($admin_route === 'login') {
|
||||
include ROOT_PATH . 'admin/login.php';
|
||||
} elseif ($admin_route === 'logout') {
|
||||
include ROOT_PATH . 'admin/logout.php';
|
||||
} elseif ($admin_route === 'post') {
|
||||
include ROOT_PATH . 'admin/post.php';
|
||||
} elseif ($admin_route === 'category') {
|
||||
include ROOT_PATH . 'admin/category.php';
|
||||
} elseif ($admin_route === 'comment') {
|
||||
include ROOT_PATH . 'admin/comment.php';
|
||||
} elseif ($admin_route === 'settings') {
|
||||
include ROOT_PATH . 'admin/settings.php';
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo '管理员页面不存在';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Post routes
|
||||
if (strpos($route, 'post/') === 0) {
|
||||
$slug = substr($route, 5);
|
||||
$_GET['slug'] = $slug;
|
||||
include TEMPLATE_PATH . 'flow/post.php';
|
||||
return;
|
||||
}
|
||||
|
||||
// Category routes
|
||||
if (strpos($route, 'category/') === 0) {
|
||||
$slug = substr($route, 9);
|
||||
$_GET['slug'] = $slug;
|
||||
include TEMPLATE_PATH . 'flow/category.php';
|
||||
return;
|
||||
}
|
||||
|
||||
// Archive route
|
||||
if ($route === 'archive') {
|
||||
include TEMPLATE_PATH . 'flow/archive.php';
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for static files
|
||||
if (file_exists(ROOT_PATH . $route)) {
|
||||
return false; // Let Apache serve it
|
||||
}
|
||||
|
||||
// 404
|
||||
http_response_code(404);
|
||||
echo '页面不存在';
|
||||
return true;
|
||||
}
|
||||
21
static/css/style.css
Normal file
21
static/css/style.css
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/* ZhangPu Blog - Base Styles */
|
||||
:root {
|
||||
--bg: #fafafa;
|
||||
--surface: #fff;
|
||||
--border: #e5e5e5;
|
||||
--text: #333;
|
||||
--accent: #22c55e;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
}
|
||||
26
static/js/script.js
Normal file
26
static/js/script.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* ZhangPu Blog - Base JavaScript
|
||||
*/
|
||||
|
||||
// Mobile menu toggle
|
||||
function toggleMobileMenu() {
|
||||
var nav = document.querySelector('.main-nav');
|
||||
if (nav) {
|
||||
nav.classList.toggle('show');
|
||||
}
|
||||
}
|
||||
|
||||
// Add event listeners when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Auto-hide messages after 5 seconds
|
||||
var messages = document.querySelectorAll('.success, .error');
|
||||
messages.forEach(function(msg) {
|
||||
setTimeout(function() {
|
||||
msg.style.opacity = '0';
|
||||
msg.style.transition = 'opacity 0.5s';
|
||||
setTimeout(function() {
|
||||
msg.remove();
|
||||
}, 500);
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
94
templates/flow/archive.php
Normal file
94
templates/flow/archive.php
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
/**
|
||||
* Flow Template - Archive Page
|
||||
*/
|
||||
require_once __DIR__ . '/../../config.php';
|
||||
require_once INCLUDE_PATH . 'function.common.php';
|
||||
require_once INCLUDE_PATH . 'function.post.php';
|
||||
require_once INCLUDE_PATH . 'function.category.php';
|
||||
|
||||
$base_url = '';
|
||||
$page_title = '归档';
|
||||
|
||||
$db = get_db();
|
||||
$result = $db->query("SELECT id, title, slug, created_at FROM posts WHERE status = 'published' AND deleted_at IS NULL ORDER BY created_at DESC");
|
||||
$all_posts = $result->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
// Group by year and month
|
||||
$archives = [];
|
||||
foreach ($all_posts as $post) {
|
||||
$year = date('Y', strtotime($post['created_at']));
|
||||
$month = date('m', strtotime($post['created_at']));
|
||||
if (!isset($archives[$year])) {
|
||||
$archives[$year] = [];
|
||||
}
|
||||
if (!isset($archives[$year][$month])) {
|
||||
$archives[$year][$month] = [];
|
||||
}
|
||||
$archives[$year][$month][] = $post;
|
||||
}
|
||||
|
||||
$categories = get_categories();
|
||||
$recent_posts = get_recent_posts(5);
|
||||
|
||||
include __DIR__ . '/header.php';
|
||||
?>
|
||||
|
||||
<div class="main-area">
|
||||
<div class="archive-header">
|
||||
<h1>📚 文章归档</h1>
|
||||
<p class="archive-stats">共 <?php echo count($all_posts); ?> 篇文章</p>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($archives)): ?>
|
||||
<?php foreach ($archives as $year => $months): ?>
|
||||
<div class="archive-year">
|
||||
<h2 class="year-title"><?php echo $year; ?> 年</h2>
|
||||
<?php foreach ($months as $month => $posts): ?>
|
||||
<div class="archive-month">
|
||||
<h3 class="month-title"><?php echo $month; ?> 月</h3>
|
||||
<ul class="archive-list">
|
||||
<?php foreach ($posts as $post): ?>
|
||||
<li>
|
||||
<span class="archive-date"><?php echo date('m-d', strtotime($post['created_at'])); ?></span>
|
||||
<a href="<?php echo $base_url; ?>post/<?php echo e($post['slug']); ?>" class="archive-title"><?php echo e($post['title']); ?></a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<div class="empty-state">
|
||||
<p>暂无文章</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<aside class="sidebar-area">
|
||||
<div class="sidebar-widget">
|
||||
<h3>📁 分类</h3>
|
||||
<ul class="category-list">
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<li>
|
||||
<a href="<?php echo $base_url; ?>category/<?php echo e($cat['slug']); ?>">
|
||||
<span class="cat-name"><?php echo e($cat['name']); ?></span>
|
||||
<span class="cat-count"><?php echo get_category_count($cat['id']); ?></span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-widget">
|
||||
<h3>📝 最新文章</h3>
|
||||
<ul class="recent-list">
|
||||
<?php foreach ($recent_posts as $recent): ?>
|
||||
<li><a href="<?php echo $base_url; ?>post/<?php echo e($recent['slug']); ?>"><?php echo e($recent['title']); ?></a></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<?php include __DIR__ . '/footer.php'; ?>
|
||||
110
templates/flow/category.php
Normal file
110
templates/flow/category.php
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
/**
|
||||
* Flow Template - Category Page
|
||||
*/
|
||||
require_once __DIR__ . '/../../config.php';
|
||||
require_once INCLUDE_PATH . 'function.common.php';
|
||||
require_once INCLUDE_PATH . 'function.post.php';
|
||||
require_once INCLUDE_PATH . 'function.category.php';
|
||||
|
||||
$base_url = '';
|
||||
$slug = $_GET['slug'] ?? '';
|
||||
|
||||
if (empty($slug)) {
|
||||
http_response_code(404);
|
||||
echo '分类不存在';
|
||||
exit;
|
||||
}
|
||||
|
||||
$category = get_category_by_slug($slug);
|
||||
|
||||
if (!$category) {
|
||||
http_response_code(404);
|
||||
echo '分类不存在';
|
||||
exit;
|
||||
}
|
||||
|
||||
$page_title = $category['name'];
|
||||
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
||||
$per_page = intval(get_setting('posts_per_page', '10'));
|
||||
|
||||
$posts = get_posts($page, $per_page, $category['id']);
|
||||
$total_posts = get_category_count($category['id']);
|
||||
$total_pages = ceil($total_posts / $per_page);
|
||||
|
||||
$recent_posts = get_recent_posts(5);
|
||||
$categories = get_categories();
|
||||
|
||||
include __DIR__ . '/header.php';
|
||||
?>
|
||||
|
||||
<div class="main-area">
|
||||
<div class="category-header">
|
||||
<h1>📁 <?php echo e($category['name']); ?></h1>
|
||||
<?php if ($category['description']): ?>
|
||||
<p class="category-desc"><?php echo e($category['description']); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (empty($posts)): ?>
|
||||
<div class="empty-state">
|
||||
<p>该分类下暂无文章</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($posts as $post): ?>
|
||||
<article class="post-card">
|
||||
<div class="post-meta">
|
||||
<span class="post-date"><?php echo date('Y-m-d', strtotime($post['created_at'])); ?></span>
|
||||
<span class="post-views">👁 <?php echo $post['views']; ?></span>
|
||||
</div>
|
||||
<h2 class="post-title"><a href="<?php echo $base_url; ?>post/<?php echo e($post['slug']); ?>"><?php echo e($post['title']); ?></a></h2>
|
||||
<?php if ($post['excerpt']): ?>
|
||||
<p class="post-excerpt"><?php echo e($post['excerpt']); ?></p>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if ($total_pages > 1): ?>
|
||||
<nav class="pagination">
|
||||
<?php if ($page > 1): ?>
|
||||
<a href="?slug=<?php echo e($slug); ?>&page=<?php echo $page - 1; ?>" class="prev">← 上一页</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php for ($i = max(1, $page - 2); $i <= min($total_pages, $page + 2); $i++): ?>
|
||||
<a href="?slug=<?php echo e($slug); ?>&page=<?php echo $i; ?>" class="<?php echo $i === $page ? 'active' : ''; ?>"><?php echo $i; ?></a>
|
||||
<?php endfor; ?>
|
||||
|
||||
<?php if ($page < $total_pages): ?>
|
||||
<a href="?slug=<?php echo e($slug); ?>&page=<?php echo $page + 1; ?>" class="next">下一页 →</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<aside class="sidebar-area">
|
||||
<div class="sidebar-widget">
|
||||
<h3>📁 分类</h3>
|
||||
<ul class="category-list">
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<li>
|
||||
<a href="<?php echo $base_url; ?>category/<?php echo e($cat['slug']); ?>" class="<?php echo $cat['id'] === $category['id'] ? 'active' : ''; ?>">
|
||||
<span class="cat-name"><?php echo e($cat['name']); ?></span>
|
||||
<span class="cat-count"><?php echo get_category_count($cat['id']); ?></span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-widget">
|
||||
<h3>📝 最新文章</h3>
|
||||
<ul class="recent-list">
|
||||
<?php foreach ($recent_posts as $recent): ?>
|
||||
<li><a href="<?php echo $base_url; ?>post/<?php echo e($recent['slug']); ?>"><?php echo e($recent['title']); ?></a></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<?php include __DIR__ . '/footer.php'; ?>
|
||||
35
templates/flow/footer.php
Normal file
35
templates/flow/footer.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
/**
|
||||
* Flow Template - Footer
|
||||
*/
|
||||
$site_name = get_setting('site_name', 'TBlog');
|
||||
$github_url = 'https://github.com/tblog-cn/tblog';
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<div class="footer-links">
|
||||
<a href="https://tblog.cn" target="_blank">TBlog</a>
|
||||
<span class="sep">·</span>
|
||||
<a href="<?php echo $github_url; ?>" target="_blank" title="GitHub">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" style="vertical-align:middle">
|
||||
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
|
||||
</svg>
|
||||
开源
|
||||
</a>
|
||||
<span class="sep">·</span>
|
||||
<a href="https://github.com/tblog-cn/tblog/releases" target="_blank">更新日志</a>
|
||||
</div>
|
||||
<p>© <?php echo date('Y'); ?> <?php echo e($site_name); ?> · MIT License</p>
|
||||
</div>
|
||||
</footer>
|
||||
<script>
|
||||
function toggleMobileMenu() {
|
||||
var nav = document.querySelector('.main-nav');
|
||||
nav.classList.toggle('show');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
42
templates/flow/header.php
Normal file
42
templates/flow/header.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
/**
|
||||
* Flow Template - Header
|
||||
*/
|
||||
$base_url = '/';
|
||||
require_once __DIR__ . '/../../config.php';
|
||||
require_once INCLUDE_PATH . 'function.common.php';
|
||||
require_once INCLUDE_PATH . 'function.category.php';
|
||||
|
||||
$site_name = get_setting('site_name', 'TBlog');
|
||||
$site_description = get_setting('site_description', '一个简洁优雅的开源博客系统');
|
||||
$categories = get_categories();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo isset($page_title) ? e($page_title) . ' - ' : ''; ?><?php echo e($site_name); ?></title>
|
||||
<meta name="description" content="<?php echo e($site_description); ?>">
|
||||
<link rel="stylesheet" href="<?php echo $base_url ?? ''; ?>static/css/style.css">
|
||||
<link rel="stylesheet" href="<?php echo $base_url ?? ''; ?>templates/flow/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="container">
|
||||
<div class="header-inner">
|
||||
<a href="<?php echo $base_url ?? ''; ?>" class="logo">🌿 <?php echo e($site_name); ?></a>
|
||||
<nav class="main-nav">
|
||||
<a href="<?php echo $base_url ?? ''; ?>">首页</a>
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<a href="<?php echo ($base_url ?? '') . 'category/' . e($cat['slug']); ?>"><?php echo e($cat['name']); ?></a>
|
||||
<?php endforeach; ?>
|
||||
<a href="<?php echo ($base_url ?? '') . 'admin/'; ?>" class="admin-link">管理</a>
|
||||
</nav>
|
||||
<button class="mobile-menu-btn" onclick="toggleMobileMenu()">☰</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="main-content">
|
||||
<div class="container">
|
||||
<div class="content-wrapper">
|
||||
89
templates/flow/index.php
Normal file
89
templates/flow/index.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
/**
|
||||
* Flow Template - Home Page
|
||||
*/
|
||||
require_once __DIR__ . '/../../config.php';
|
||||
require_once INCLUDE_PATH . 'function.common.php';
|
||||
require_once INCLUDE_PATH . 'function.post.php';
|
||||
require_once INCLUDE_PATH . 'function.category.php';
|
||||
|
||||
$base_url = '/';
|
||||
$page_title = '首页';
|
||||
|
||||
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
||||
$per_page = intval(get_setting('posts_per_page', '10'));
|
||||
|
||||
$posts = get_posts($page, $per_page);
|
||||
$total_posts = get_total_posts();
|
||||
$total_pages = ceil($total_posts / $per_page);
|
||||
|
||||
$recent_posts = get_recent_posts(5);
|
||||
$categories = get_categories();
|
||||
|
||||
include __DIR__ . '/header.php';
|
||||
?>
|
||||
|
||||
<div class="main-area">
|
||||
<?php if (empty($posts)): ?>
|
||||
<div class="empty-state">
|
||||
<p>暂无文章</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($posts as $post): ?>
|
||||
<article class="post-card">
|
||||
<div class="post-meta">
|
||||
<a href="<?php echo $base_url; ?>category/<?php echo e($post['category_slug']); ?>" class="category-tag"><?php echo e($post['category_name']); ?></a>
|
||||
<span class="post-date"><?php echo date('Y-m-d', strtotime($post['created_at'])); ?></span>
|
||||
<span class="post-views">👁 <?php echo $post['views']; ?></span>
|
||||
</div>
|
||||
<h2 class="post-title"><a href="<?php echo $base_url; ?>post/<?php echo e($post['slug']); ?>"><?php echo e($post['title']); ?></a></h2>
|
||||
<?php if ($post['excerpt']): ?>
|
||||
<p class="post-excerpt"><?php echo e($post['excerpt']); ?></p>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if ($total_pages > 1): ?>
|
||||
<nav class="pagination">
|
||||
<?php if ($page > 1): ?>
|
||||
<a href="?page=<?php echo $page - 1; ?>" class="prev">← 上一页</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php for ($i = max(1, $page - 2); $i <= min($total_pages, $page + 2); $i++): ?>
|
||||
<a href="?page=<?php echo $i; ?>" class="<?php echo $i === $page ? 'active' : ''; ?>"><?php echo $i; ?></a>
|
||||
<?php endfor; ?>
|
||||
|
||||
<?php if ($page < $total_pages): ?>
|
||||
<a href="?page=<?php echo $page + 1; ?>" class="next">下一页 →</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<aside class="sidebar-area">
|
||||
<div class="sidebar-widget">
|
||||
<h3>📁 分类</h3>
|
||||
<ul class="category-list">
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<li>
|
||||
<a href="<?php echo $base_url; ?>category/<?php echo e($cat['slug']); ?>">
|
||||
<span class="cat-name"><?php echo e($cat['name']); ?></span>
|
||||
<span class="cat-count"><?php echo get_category_count($cat['id']); ?></span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-widget">
|
||||
<h3>📝 最新文章</h3>
|
||||
<ul class="recent-list">
|
||||
<?php foreach ($recent_posts as $recent): ?>
|
||||
<li><a href="<?php echo $base_url; ?>post/<?php echo e($recent['slug']); ?>"><?php echo e($recent['title']); ?></a></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<?php include __DIR__ . '/footer.php'; ?>
|
||||
108
templates/flow/post.php
Normal file
108
templates/flow/post.php
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
/**
|
||||
* Flow Template - Single Post
|
||||
*/
|
||||
require_once __DIR__ . '/../../config.php';
|
||||
require_once INCLUDE_PATH . 'function.common.php';
|
||||
require_once INCLUDE_PATH . 'function.post.php';
|
||||
require_once INCLUDE_PATH . 'function.comment.php';
|
||||
|
||||
$base_url = '';
|
||||
$slug = $_GET['slug'] ?? '';
|
||||
|
||||
if (empty($slug)) {
|
||||
http_response_code(404);
|
||||
echo '文章不存在';
|
||||
exit;
|
||||
}
|
||||
|
||||
$post = get_post_by_slug($slug);
|
||||
|
||||
if (!$post) {
|
||||
http_response_code(404);
|
||||
echo '文章不存在';
|
||||
exit;
|
||||
}
|
||||
|
||||
$page_title = $post['title'];
|
||||
$comments = get_comments($post['id']);
|
||||
|
||||
// Handle comment submission
|
||||
$comment_message = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['action']) && $_POST['action'] === 'add_comment') {
|
||||
$data = [
|
||||
'post_id' => $post['id'],
|
||||
'author' => trim($_POST['author'] ?? ''),
|
||||
'email' => trim($_POST['email'] ?? ''),
|
||||
'url' => trim($_POST['url'] ?? ''),
|
||||
'content' => trim($_POST['content'] ?? '')
|
||||
];
|
||||
|
||||
if (empty($data['author']) || empty($data['content'])) {
|
||||
$comment_message = '<div class="comment-error">请填写昵称和评论内容</div>';
|
||||
} else {
|
||||
if (create_comment($data)) {
|
||||
$comment_message = '<div class="comment-success">评论已提交,等待审核</div>';
|
||||
$data = ['post_id' => $post['id'], 'author' => '', 'email' => '', 'url' => '', 'content' => ''];
|
||||
} else {
|
||||
$comment_message = '<div class="comment-error">评论提交失败</div>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$data = ['post_id' => $post['id'], 'author' => '', 'email' => '', 'url' => '', 'content' => ''];
|
||||
}
|
||||
|
||||
include __DIR__ . '/header.php';
|
||||
?>
|
||||
|
||||
<article class="post-single">
|
||||
<header class="post-header">
|
||||
<div class="post-meta">
|
||||
<a href="<?php echo $base_url; ?>category/<?php echo e($post['category_slug']); ?>" class="category-tag"><?php echo e($post['category_name']); ?></a>
|
||||
<span class="post-date"><?php echo date('Y-m-d', strtotime($post['created_at'])); ?></span>
|
||||
<span class="post-views">👁 <?php echo $post['views']; ?> 次阅读</span>
|
||||
</div>
|
||||
<h1 class="post-title"><?php echo e($post['title']); ?></h1>
|
||||
</header>
|
||||
|
||||
<div class="post-content">
|
||||
<?php echo $post['content']; ?>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<section class="comments-section">
|
||||
<h3>💬 评论 (<?php echo count($comments); ?>)</h3>
|
||||
|
||||
<?php if (!empty($comment_message)): ?>
|
||||
<?php echo $comment_message; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="comment-list">
|
||||
<?php if (!empty($comments)): ?>
|
||||
<?php foreach ($comments as $comment): ?>
|
||||
<div class="comment-item">
|
||||
<div class="comment-author"><?php echo e($comment['author']); ?></div>
|
||||
<div class="comment-date"><?php echo date('Y-m-d H:i', strtotime($comment['created_at'])); ?></div>
|
||||
<div class="comment-content"><?php echo e($comment['content']); ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<p class="no-comments">暂无评论,来说两句吧~</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form method="POST" class="comment-form">
|
||||
<input type="hidden" name="action" value="add_comment">
|
||||
<input type="text" name="website" style="display:none" tabindex="-1" autocomplete="off">
|
||||
<h4>发表评论</h4>
|
||||
<div class="form-row">
|
||||
<input type="text" name="author" placeholder="昵称 *" value="<?php echo e($data['author']); ?>" required>
|
||||
<input type="email" name="email" placeholder="邮箱" value="<?php echo e($data['email']); ?>">
|
||||
</div>
|
||||
<input type="url" name="url" placeholder="网站" value="<?php echo e($data['url']); ?>">
|
||||
<textarea name="content" rows="4" placeholder="评论内容 *" required><?php echo e($data['content']); ?></textarea>
|
||||
<button type="submit" class="btn-submit">提交评论</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<?php include __DIR__ . '/footer.php'; ?>
|
||||
543
templates/flow/static/css/style.css
Normal file
543
templates/flow/static/css/style.css
Normal file
|
|
@ -0,0 +1,543 @@
|
|||
:root {
|
||||
--bg: #fafafa;
|
||||
--surface: #fff;
|
||||
--border: #e5e5e5;
|
||||
--text: #333;
|
||||
--text-secondary: #666;
|
||||
--accent: #22c55e;
|
||||
--accent2: #0ea5e9;
|
||||
--shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
--radius: 12px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.site-header {
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.main-nav {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.main-nav a {
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
padding: 8px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-nav a:hover,
|
||||
.main-nav a.active {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.main-nav a::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
transition: width 0.2s;
|
||||
}
|
||||
|
||||
.main-nav a:hover::after,
|
||||
.main-nav a.active::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.admin-link {
|
||||
color: var(--text-secondary) !important;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mobile-menu-btn {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Content Layout */
|
||||
.main-content {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 300px;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.main-area {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* Post Card */
|
||||
.post-card {
|
||||
background: var(--surface);
|
||||
padding: 24px;
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 20px;
|
||||
box-shadow: var(--shadow);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.post-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.category-tag {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.category-tag:hover {
|
||||
background: #16a34a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
font-size: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.post-title a {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.post-title a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.post-excerpt {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* Sidebar Widgets */
|
||||
.sidebar-widget {
|
||||
background: var(--surface);
|
||||
padding: 20px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.sidebar-widget h3 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
.category-list,
|
||||
.recent-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.category-list li a,
|
||||
.recent-list li a {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
color: var(--text);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.category-list li:last-child a,
|
||||
.recent-list li:last-child a {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.category-list li a:hover,
|
||||
.recent-list li a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.cat-count {
|
||||
background: var(--bg);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Single Post */
|
||||
.post-single {
|
||||
background: var(--surface);
|
||||
padding: 40px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.post-header {
|
||||
margin-bottom: 32px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.post-header .post-meta {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.post-header .post-title {
|
||||
font-size: 32px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
line-height: 1.8;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.post-content p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Comments */
|
||||
.comments-section {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.comments-section h3 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.comment-list {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
background: var(--surface);
|
||||
padding: 20px;
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin: 4px 0 12px;
|
||||
}
|
||||
|
||||
.comment-form {
|
||||
background: var(--surface);
|
||||
padding: 24px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.comment-form h4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.comment-form .form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.comment-form input,
|
||||
.comment-form textarea {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
transition: border-color 0.2s;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.comment-form input:focus,
|
||||
.comment-form textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 12px 32px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-submit:hover {
|
||||
background: #16a34a;
|
||||
}
|
||||
|
||||
.comment-success {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.comment-error {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.no-comments {
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.pagination a {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
background: var(--surface);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
box-shadow: var(--shadow);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.pagination a:hover {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.pagination a.active {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Archive */
|
||||
.archive-header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.archive-stats {
|
||||
color: var(--text-secondary);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.archive-year {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.year-title {
|
||||
font-size: 28px;
|
||||
color: var(--accent);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.archive-month {
|
||||
margin-bottom: 24px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.month-title {
|
||||
font-size: 18px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.archive-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.archive-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.archive-date {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
width: 60px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.archive-title {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.archive-title:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Category Header */
|
||||
.category-header {
|
||||
background: var(--surface);
|
||||
padding: 32px;
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 24px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.category-desc {
|
||||
color: var(--text-secondary);
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
background: var(--surface);
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
border-radius: var(--radius);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.site-footer {
|
||||
background: var(--surface);
|
||||
padding: 24px 0;
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 60px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 900px) {
|
||||
.content-wrapper {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar-area {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.main-area {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.main-nav {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--surface);
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.main-nav.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mobile-menu-btn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.post-header .post-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.post-single {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.comment-form .form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
110
templates/magine/category.php
Normal file
110
templates/magine/category.php
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
/**
|
||||
* Magine Template - Category Page (Dark Theme)
|
||||
*/
|
||||
require_once __DIR__ . '/../../config.php';
|
||||
require_once INCLUDE_PATH . 'function.common.php';
|
||||
require_once INCLUDE_PATH . 'function.post.php';
|
||||
require_once INCLUDE_PATH . 'function.category.php';
|
||||
|
||||
$base_url = '';
|
||||
$slug = $_GET['slug'] ?? '';
|
||||
|
||||
if (empty($slug)) {
|
||||
http_response_code(404);
|
||||
echo '分类不存在';
|
||||
exit;
|
||||
}
|
||||
|
||||
$category = get_category_by_slug($slug);
|
||||
|
||||
if (!$category) {
|
||||
http_response_code(404);
|
||||
echo '分类不存在';
|
||||
exit;
|
||||
}
|
||||
|
||||
$page_title = $category['name'];
|
||||
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
||||
$per_page = intval(get_setting('posts_per_page', '10'));
|
||||
|
||||
$posts = get_posts($page, $per_page, $category['id']);
|
||||
$total_posts = get_category_count($category['id']);
|
||||
$total_pages = ceil($total_posts / $per_page);
|
||||
|
||||
$recent_posts = get_recent_posts(5);
|
||||
$categories = get_categories();
|
||||
|
||||
include __DIR__ . '/header.php';
|
||||
?>
|
||||
|
||||
<div class="main-area">
|
||||
<div class="category-header">
|
||||
<h1>📁 <?php echo e($category['name']); ?></h1>
|
||||
<?php if ($category['description']): ?>
|
||||
<p class="category-desc"><?php echo e($category['description']); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (empty($posts)): ?>
|
||||
<div class="empty-state">
|
||||
<p>该分类下暂无文章</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($posts as $post): ?>
|
||||
<article class="post-card">
|
||||
<div class="post-meta">
|
||||
<span class="post-date"><?php echo date('Y-m-d', strtotime($post['created_at'])); ?></span>
|
||||
<span class="post-views">👁 <?php echo $post['views']; ?></span>
|
||||
</div>
|
||||
<h2 class="post-title"><a href="<?php echo $base_url; ?>post/<?php echo e($post['slug']); ?>"><?php echo e($post['title']); ?></a></h2>
|
||||
<?php if ($post['excerpt']): ?>
|
||||
<p class="post-excerpt"><?php echo e($post['excerpt']); ?></p>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if ($total_pages > 1): ?>
|
||||
<nav class="pagination">
|
||||
<?php if ($page > 1): ?>
|
||||
<a href="?slug=<?php echo e($slug); ?>&page=<?php echo $page - 1; ?>" class="prev">← 上一页</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php for ($i = max(1, $page - 2); $i <= min($total_pages, $page + 2); $i++): ?>
|
||||
<a href="?slug=<?php echo e($slug); ?>&page=<?php echo $i; ?>" class="<?php echo $i === $page ? 'active' : ''; ?>"><?php echo $i; ?></a>
|
||||
<?php endfor; ?>
|
||||
|
||||
<?php if ($page < $total_pages): ?>
|
||||
<a href="?slug=<?php echo e($slug); ?>&page=<?php echo $page + 1; ?>" class="next">下一页 →</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<aside class="sidebar-area">
|
||||
<div class="sidebar-widget">
|
||||
<h3>📁 分类</h3>
|
||||
<ul class="category-list">
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<li>
|
||||
<a href="<?php echo $base_url; ?>category/<?php echo e($cat['slug']); ?>" class="<?php echo $cat['id'] === $category['id'] ? 'active' : ''; ?>">
|
||||
<span class="cat-name"><?php echo e($cat['name']); ?></span>
|
||||
<span class="cat-count"><?php echo get_category_count($cat['id']); ?></span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-widget">
|
||||
<h3>📝 最新文章</h3>
|
||||
<ul class="recent-list">
|
||||
<?php foreach ($recent_posts as $recent): ?>
|
||||
<li><a href="<?php echo $base_url; ?>post/<?php echo e($recent['slug']); ?>"><?php echo e($recent['title']); ?></a></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<?php include __DIR__ . '/footer.php'; ?>
|
||||
35
templates/magine/footer.php
Normal file
35
templates/magine/footer.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
/**
|
||||
* Magine Template - Footer (Dark Theme)
|
||||
*/
|
||||
$site_name = get_setting('site_name', 'TBlog');
|
||||
$github_url = 'https://github.com/tblog-cn/tblog';
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<div class="footer-links">
|
||||
<a href="https://tblog.cn" target="_blank">TBlog</a>
|
||||
<span class="sep">·</span>
|
||||
<a href="<?php echo $github_url; ?>" target="_blank" title="GitHub">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" style="vertical-align:middle">
|
||||
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
|
||||
</svg>
|
||||
开源
|
||||
</a>
|
||||
<span class="sep">·</span>
|
||||
<a href="https://github.com/tblog-cn/tblog/releases" target="_blank">更新日志</a>
|
||||
</div>
|
||||
<p>© <?php echo date('Y'); ?> <?php echo e($site_name); ?> · MIT License</p>
|
||||
</div>
|
||||
</footer>
|
||||
<script>
|
||||
function toggleMobileMenu() {
|
||||
var nav = document.querySelector('.main-nav');
|
||||
nav.classList.toggle('show');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
40
templates/magine/header.php
Normal file
40
templates/magine/header.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
/**
|
||||
* Magine Template - Header (Dark Theme)
|
||||
*/
|
||||
require_once __DIR__ . '/../../config.php';
|
||||
require_once INCLUDE_PATH . 'function.common.php';
|
||||
require_once INCLUDE_PATH . 'function.category.php';
|
||||
|
||||
$site_name = get_setting('site_name', 'TBlog');
|
||||
$site_description = get_setting('site_description', '一个简洁优雅的开源博客系统');
|
||||
$categories = get_categories();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo isset($page_title) ? e($page_title) . ' - ' : ''; ?><?php echo e($site_name); ?></title>
|
||||
<meta name="description" content="<?php echo e($site_description); ?>">
|
||||
<link rel="stylesheet" href="<?php echo $base_url ?? ''; ?>templates/magine/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="container">
|
||||
<div class="header-inner">
|
||||
<a href="<?php echo $base_url ?? ''; ?>" class="logo">🌙 <?php echo e($site_name); ?></a>
|
||||
<nav class="main-nav">
|
||||
<a href="<?php echo $base_url ?? ''; ?>">首页</a>
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<a href="<?php echo ($base_url ?? '') . 'category/' . e($cat['slug']); ?>"><?php echo e($cat['name']); ?></a>
|
||||
<?php endforeach; ?>
|
||||
<a href="<?php echo ($base_url ?? '') . 'admin/'; ?>" class="admin-link">管理</a>
|
||||
</nav>
|
||||
<button class="mobile-menu-btn" onclick="toggleMobileMenu()">☰</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="main-content">
|
||||
<div class="container">
|
||||
<div class="content-wrapper">
|
||||
89
templates/magine/index.php
Normal file
89
templates/magine/index.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
/**
|
||||
* Magine Template - Home Page (Dark Theme)
|
||||
*/
|
||||
require_once __DIR__ . '/../../config.php';
|
||||
require_once INCLUDE_PATH . 'function.common.php';
|
||||
require_once INCLUDE_PATH . 'function.post.php';
|
||||
require_once INCLUDE_PATH . 'function.category.php';
|
||||
|
||||
$base_url = '';
|
||||
$page_title = '首页';
|
||||
|
||||
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
||||
$per_page = intval(get_setting('posts_per_page', '10'));
|
||||
|
||||
$posts = get_posts($page, $per_page);
|
||||
$total_posts = get_total_posts();
|
||||
$total_pages = ceil($total_posts / $per_page);
|
||||
|
||||
$recent_posts = get_recent_posts(5);
|
||||
$categories = get_categories();
|
||||
|
||||
include __DIR__ . '/header.php';
|
||||
?>
|
||||
|
||||
<div class="main-area">
|
||||
<?php if (empty($posts)): ?>
|
||||
<div class="empty-state">
|
||||
<p>暂无文章</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($posts as $post): ?>
|
||||
<article class="post-card">
|
||||
<div class="post-meta">
|
||||
<a href="<?php echo $base_url; ?>category/<?php echo e($post['category_slug']); ?>" class="category-tag"><?php echo e($post['category_name']); ?></a>
|
||||
<span class="post-date"><?php echo date('Y-m-d', strtotime($post['created_at'])); ?></span>
|
||||
<span class="post-views">👁 <?php echo $post['views']; ?></span>
|
||||
</div>
|
||||
<h2 class="post-title"><a href="<?php echo $base_url; ?>post/<?php echo e($post['slug']); ?>"><?php echo e($post['title']); ?></a></h2>
|
||||
<?php if ($post['excerpt']): ?>
|
||||
<p class="post-excerpt"><?php echo e($post['excerpt']); ?></p>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if ($total_pages > 1): ?>
|
||||
<nav class="pagination">
|
||||
<?php if ($page > 1): ?>
|
||||
<a href="?page=<?php echo $page - 1; ?>" class="prev">← 上一页</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php for ($i = max(1, $page - 2); $i <= min($total_pages, $page + 2); $i++): ?>
|
||||
<a href="?page=<?php echo $i; ?>" class="<?php echo $i === $page ? 'active' : ''; ?>"><?php echo $i; ?></a>
|
||||
<?php endfor; ?>
|
||||
|
||||
<?php if ($page < $total_pages): ?>
|
||||
<a href="?page=<?php echo $page + 1; ?>" class="next">下一页 →</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<aside class="sidebar-area">
|
||||
<div class="sidebar-widget">
|
||||
<h3>📁 分类</h3>
|
||||
<ul class="category-list">
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<li>
|
||||
<a href="<?php echo $base_url; ?>category/<?php echo e($cat['slug']); ?>">
|
||||
<span class="cat-name"><?php echo e($cat['name']); ?></span>
|
||||
<span class="cat-count"><?php echo get_category_count($cat['id']); ?></span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-widget">
|
||||
<h3>📝 最新文章</h3>
|
||||
<ul class="recent-list">
|
||||
<?php foreach ($recent_posts as $recent): ?>
|
||||
<li><a href="<?php echo $base_url; ?>post/<?php echo e($recent['slug']); ?>"><?php echo e($recent['title']); ?></a></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<?php include __DIR__ . '/footer.php'; ?>
|
||||
107
templates/magine/post.php
Normal file
107
templates/magine/post.php
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
/**
|
||||
* Magine Template - Single Post (Dark Theme)
|
||||
*/
|
||||
require_once __DIR__ . '/../../config.php';
|
||||
require_once INCLUDE_PATH . 'function.common.php';
|
||||
require_once INCLUDE_PATH . 'function.post.php';
|
||||
require_once INCLUDE_PATH . 'function.comment.php';
|
||||
|
||||
$base_url = '';
|
||||
$slug = $_GET['slug'] ?? '';
|
||||
|
||||
if (empty($slug)) {
|
||||
http_response_code(404);
|
||||
echo '文章不存在';
|
||||
exit;
|
||||
}
|
||||
|
||||
$post = get_post_by_slug($slug);
|
||||
|
||||
if (!$post) {
|
||||
http_response_code(404);
|
||||
echo '文章不存在';
|
||||
exit;
|
||||
}
|
||||
|
||||
$page_title = $post['title'];
|
||||
$comments = get_comments($post['id']);
|
||||
|
||||
$comment_message = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['action']) && $_POST['action'] === 'add_comment') {
|
||||
$data = [
|
||||
'post_id' => $post['id'],
|
||||
'author' => trim($_POST['author'] ?? ''),
|
||||
'email' => trim($_POST['email'] ?? ''),
|
||||
'url' => trim($_POST['url'] ?? ''),
|
||||
'content' => trim($_POST['content'] ?? '')
|
||||
];
|
||||
|
||||
if (empty($data['author']) || empty($data['content'])) {
|
||||
$comment_message = '<div class="comment-error">请填写昵称和评论内容</div>';
|
||||
} else {
|
||||
if (create_comment($data)) {
|
||||
$comment_message = '<div class="comment-success">评论已提交,等待审核</div>';
|
||||
$data = ['post_id' => $post['id'], 'author' => '', 'email' => '', 'url' => '', 'content' => ''];
|
||||
} else {
|
||||
$comment_message = '<div class="comment-error">评论提交失败</div>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$data = ['post_id' => $post['id'], 'author' => '', 'email' => '', 'url' => '', 'content' => ''];
|
||||
}
|
||||
|
||||
include __DIR__ . '/header.php';
|
||||
?>
|
||||
|
||||
<article class="post-single">
|
||||
<header class="post-header">
|
||||
<div class="post-meta">
|
||||
<a href="<?php echo $base_url; ?>category/<?php echo e($post['category_slug']); ?>" class="category-tag"><?php echo e($post['category_name']); ?></a>
|
||||
<span class="post-date"><?php echo date('Y-m-d', strtotime($post['created_at'])); ?></span>
|
||||
<span class="post-views">👁 <?php echo $post['views']; ?> 次阅读</span>
|
||||
</div>
|
||||
<h1 class="post-title"><?php echo e($post['title']); ?></h1>
|
||||
</header>
|
||||
|
||||
<div class="post-content">
|
||||
<?php echo $post['content']; ?>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<section class="comments-section">
|
||||
<h3>💬 评论 (<?php echo count($comments); ?>)</h3>
|
||||
|
||||
<?php if (!empty($comment_message)): ?>
|
||||
<?php echo $comment_message; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="comment-list">
|
||||
<?php if (!empty($comments)): ?>
|
||||
<?php foreach ($comments as $comment): ?>
|
||||
<div class="comment-item">
|
||||
<div class="comment-author"><?php echo e($comment['author']); ?></div>
|
||||
<div class="comment-date"><?php echo date('Y-m-d H:i', strtotime($comment['created_at'])); ?></div>
|
||||
<div class="comment-content"><?php echo e($comment['content']); ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<p class="no-comments">暂无评论,来说两句吧~</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form method="POST" class="comment-form">
|
||||
<input type="hidden" name="action" value="add_comment">
|
||||
<input type="text" name="website" style="display:none" tabindex="-1" autocomplete="off">
|
||||
<h4>发表评论</h4>
|
||||
<div class="form-row">
|
||||
<input type="text" name="author" placeholder="昵称 *" value="<?php echo e($data['author']); ?>" required>
|
||||
<input type="email" name="email" placeholder="邮箱" value="<?php echo e($data['email']); ?>">
|
||||
</div>
|
||||
<input type="url" name="url" placeholder="网站" value="<?php echo e($data['url']); ?>">
|
||||
<textarea name="content" rows="4" placeholder="评论内容 *" required><?php echo e($data['content']); ?></textarea>
|
||||
<button type="submit" class="btn-submit">提交评论</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<?php include __DIR__ . '/footer.php'; ?>
|
||||
561
templates/magine/static/css/style.css
Normal file
561
templates/magine/static/css/style.css
Normal file
|
|
@ -0,0 +1,561 @@
|
|||
:root {
|
||||
--bg: #0a0a0f;
|
||||
--surface: #12121a;
|
||||
--surface-hover: #1a1a25;
|
||||
--border: #2a2a3a;
|
||||
--text: #e0e0e8;
|
||||
--text-secondary: #8888a0;
|
||||
--accent: #4ade80;
|
||||
--accent2: #38bdf8;
|
||||
--shadow: 0 2px 8px rgba(0,0,0,0.4);
|
||||
--radius: 12px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.site-header {
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.main-nav {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.main-nav a {
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
padding: 8px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-nav a:hover,
|
||||
.main-nav a.active {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.main-nav a::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
transition: width 0.2s;
|
||||
}
|
||||
|
||||
.main-nav a:hover::after,
|
||||
.main-nav a.active::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.admin-link {
|
||||
color: var(--text-secondary) !important;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mobile-menu-btn {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Content Layout */
|
||||
.main-content {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 300px;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.main-area {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* Post Card */
|
||||
.post-card {
|
||||
background: var(--surface);
|
||||
padding: 24px;
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 20px;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.post-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.5);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.category-tag {
|
||||
background: var(--accent);
|
||||
color: #0a0a0f;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.category-tag:hover {
|
||||
background: #86efac;
|
||||
color: #0a0a0f;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
font-size: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.post-title a {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.post-title a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.post-excerpt {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* Sidebar Widgets */
|
||||
.sidebar-widget {
|
||||
background: var(--surface);
|
||||
padding: 20px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.sidebar-widget h3 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
.category-list,
|
||||
.recent-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.category-list li a,
|
||||
.recent-list li a {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
color: var(--text);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.category-list li:last-child a,
|
||||
.recent-list li:last-child a {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.category-list li a:hover,
|
||||
.recent-list li a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.cat-count {
|
||||
background: var(--bg);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Single Post */
|
||||
.post-single {
|
||||
background: var(--surface);
|
||||
padding: 40px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.post-header {
|
||||
margin-bottom: 32px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.post-header .post-meta {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.post-header .post-title {
|
||||
font-size: 32px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
line-height: 1.8;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.post-content p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Comments */
|
||||
.comments-section {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.comments-section h3 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.comment-list {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
background: var(--surface);
|
||||
padding: 20px;
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin: 4px 0 12px;
|
||||
}
|
||||
|
||||
.comment-form {
|
||||
background: var(--surface);
|
||||
padding: 24px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.comment-form h4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.comment-form .form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.comment-form input,
|
||||
.comment-form textarea {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
transition: border-color 0.2s;
|
||||
font-family: inherit;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.comment-form input:focus,
|
||||
.comment-form textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
background: var(--accent);
|
||||
color: #0a0a0f;
|
||||
border: none;
|
||||
padding: 12px 32px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-submit:hover {
|
||||
background: #86efac;
|
||||
}
|
||||
|
||||
.comment-success {
|
||||
background: rgba(74, 222, 128, 0.15);
|
||||
color: var(--accent);
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid var(--accent);
|
||||
}
|
||||
|
||||
.comment-error {
|
||||
background: rgba(248, 113, 113, 0.15);
|
||||
color: #f87171;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #f87171;
|
||||
}
|
||||
|
||||
.no-comments {
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.pagination a {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
background: var(--surface);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.pagination a:hover {
|
||||
background: var(--accent);
|
||||
color: #0a0a0f;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.pagination a.active {
|
||||
background: var(--accent);
|
||||
color: #0a0a0f;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Archive */
|
||||
.archive-header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.archive-stats {
|
||||
color: var(--text-secondary);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.archive-year {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.year-title {
|
||||
font-size: 28px;
|
||||
color: var(--accent);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.archive-month {
|
||||
margin-bottom: 24px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.month-title {
|
||||
font-size: 18px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.archive-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.archive-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.archive-date {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
width: 60px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.archive-title {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.archive-title:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Category Header */
|
||||
.category-header {
|
||||
background: var(--surface);
|
||||
padding: 32px;
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 24px;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.category-desc {
|
||||
color: var(--text-secondary);
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
background: var(--surface);
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
border-radius: var(--radius);
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.site-footer {
|
||||
background: var(--surface);
|
||||
padding: 24px 0;
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 60px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 900px) {
|
||||
.content-wrapper {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar-area {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.main-area {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.main-nav {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--surface);
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
box-shadow: var(--shadow);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.main-nav.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mobile-menu-btn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.post-header .post-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.post-single {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.comment-form .form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue