源代码 续
text/html
复制代码
{% extends 'base.html' %}
{% block title %}评论管理{% endblock %}
{% block content %}
<div class="container py-4">
<div class="row">
<div class="col-md-3">
<div class="list-group mb-4">
<a href="{{ url_for('blog.admin_posts') }}" class="list-group-item list-group-item-action">
<i class="bi bi-file-earmark-text me-2"></i> 文章管理
</a>
<a href="{{ url_for('blog.admin_categories') }}" class="list-group-item list-group-item-action">
<i class="bi bi-folder me-2"></i> 分类管理
</a>
<a href="{{ url_for('blog.admin_tags') }}" class="list-group-item list-group-item-action">
<i class="bi bi-tags me-2"></i> 标签管理
</a>
<a href="{{ url_for('blog.admin_comments') }}" class="list-group-item list-group-item-action active">
<i class="bi bi-chat-dots me-2"></i> 评论管理
</a>
{% if current_user.is_admin %}
<a href="{{ url_for('auth.user_list') }}" class="list-group-item list-group-item-action">
<i class="bi bi-people me-2"></i> 用户管理
</a>
{% endif %}
</div>
</div>
<div class="col-md-9">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">评论管理</h5>
<div class="input-group" style="width: 300px;">
<input type="text" id="commentSearchInput" class="form-control" placeholder="搜索评论...">
<button class="btn btn-outline-secondary" type="button" id="clearSearch">
<i class="bi bi-x"></i>
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th style="width: 5%">ID</th>
<th style="width: 15%">用户</th>
<th style="width: 15%">文章</th>
<th style="width: 35%">内容</th>
<th style="width: 15%">时间</th>
<th style="width: 15%">操作</th>
</tr>
</thead>
<tbody id="commentTableBody">
{% for comment in comments.items %}
<tr>
<td>{{ comment.id }}</td>
<td>
<div class="d-flex align-items-center">
{% if comment.author.avatar %}
<img src="{{ comment.author.avatar }}" alt="{{ comment.author.username }}" class="rounded-circle me-2" style="width: 32px; height: 32px; object-fit: cover;">
{% else %}
<img src="/static/images/default-avatar.png" alt="{{ comment.author.username }}" class="rounded-circle me-2" style="width: 32px; height: 32px; object-fit: cover;">
{% endif %}
{{ comment.author.username }}
</div>
</td>
<td>
<a href="{{ url_for('blog.post_detail', slug=comment.post.slug) }}" target="_blank" title="{{ comment.post.title }}">
{{ comment.post.title|truncate(20) }}
</a>
</td>
<td>
{% if comment.parent %}
<span class="badge bg-secondary me-1">回复</span>
{% endif %}
{{ comment.content|truncate(50) }}
</td>
<td>{{ comment.created_time.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<div class="btn-group">
<a href="{{ url_for('blog.post_detail', slug=comment.post.slug) }}#comment-{{ comment.id }}" class="btn btn-sm btn-outline-primary me-1" title="查看评论">
<i class="bi bi-eye"></i>
</a>
<button type="button" class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteCommentModal{{ comment.id }}" title="删除评论">
<i class="bi bi-trash"></i>
</button>
</div>
<!-- 删除评论确认模态框 -->
<div class="modal fade" id="deleteCommentModal{{ comment.id }}" tabindex="-1" aria-labelledby="deleteCommentModalLabel{{ comment.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteCommentModalLabel{{ comment.id }}">确认删除</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>您确定要删除这条评论吗?</p>
<div class="card">
<div class="card-body">
<p class="card-text">{{ comment.content }}</p>
<p class="card-text"><small class="text-muted">由 {{ comment.author.username }} 发表于 {{ comment.created_time.strftime('%Y-%m-%d %H:%M') }}</small></p>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<form action="{{ url_for('blog.admin_delete_comment', comment_id=comment.id) }}" method="POST">
<button type="submit" class="btn btn-danger">确认删除</button>
</form>
</div>
</div>
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if not comments.items %}
<div class="text-center py-4">
<p class="text-muted">暂无评论数据</p>
</div>
{% endif %}
<!-- 分页 -->
{% if comments.pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if comments.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('blog.admin_comments', page=comments.prev_num) }}" aria-label="Previous">
<span aria-hidden="true"><<</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true"><<</span>
</a>
</li>
{% endif %}
{% for page in comments.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
{% if page %}
{% if page == comments.page %}
<li class="page-item active"><a class="page-link" href="#">{{ page }}</a></li>
{% else %}
<li class="page-item"><a class="page-link" href="{{ url_for('blog.admin_comments', page=page) }}">{{ page }}</a></li>
{% endif %}
{% else %}
<li class="page-item disabled"><a class="page-link" href="#">...</a></li>
{% endif %}
{% endfor %}
{% if comments.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('blog.admin_comments', page=comments.next_num) }}" aria-label="Next">
<span aria-hidden="true">>></span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">>></span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% block scripts %}
<script>
// 评论搜索功能
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('commentSearchInput');
const clearButton = document.getElementById('clearSearch');
const tableBody = document.getElementById('commentTableBody');
const rows = tableBody.querySelectorAll('tr');
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
rows.forEach(row => {
const username = row.querySelector('td:nth-child(2)').textContent.toLowerCase();
const postTitle = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
const content = row.querySelector('td:nth-child(4)').textContent.toLowerCase();
if (username.includes(searchTerm) || postTitle.includes(searchTerm) || content.includes(searchTerm)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
clearButton.addEventListener('click', function() {
searchInput.value = '';
rows.forEach(row => {
row.style.display = '';
});
});
});
</script>
{% endblock %}
{% endblock %}
app\templates\admin\dashboard.html
text/html
复制代码
{% extends 'base.html' %}
{% block title %}管理仪表板{% endblock %}
{% block content %}
<div class="container">
<h2 class="mb-4">管理仪表板</h2>
<div class="row mb-4">
<div class="col-md-3">
<div class="card text-bg-primary mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="card-title">文章</h5>
<h2 class="card-text">{{ stats.post_count }}</h2>
</div>
<i class="bi bi-file-earmark-text fs-1"></i>
</div>
<a href="{{ url_for('blog.admin_posts') }}" class="btn btn-sm btn-light mt-2">管理文章</a>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-bg-success mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="card-title">评论</h5>
<h2 class="card-text">{{ stats.comment_count }}</h2>
</div>
<i class="bi bi-chat-dots fs-1"></i>
</div>
<a href="{{ url_for('blog.admin_comments') }}" class="btn btn-sm btn-light mt-2">管理评论</a>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-bg-info mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="card-title">用户</h5>
<h2 class="card-text">{{ stats.user_count }}</h2>
</div>
<i class="bi bi-people fs-1"></i>
</div>
<a href="{{ url_for('auth.user_list') }}" class="btn btn-sm btn-light mt-2">管理用户</a>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-bg-warning mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="card-title">分类</h5>
<h2 class="card-text">{{ stats.category_count }}</h2>
</div>
<i class="bi bi-folder fs-1"></i>
</div>
<a href="{{ url_for('blog.admin_categories') }}" class="btn btn-sm btn-light mt-2">管理分类</a>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<span>最近发布的文章</span>
<a href="{{ url_for('blog.admin_posts') }}" class="btn btn-sm btn-primary">查看全部</a>
</div>
</div>
<div class="card-body">
<div class="list-group">
{% for post in recent_posts %}
<a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ post.title }}</h5>
<small>{{ post.created_time.strftime('%Y-%m-%d') }}</small>
</div>
<p class="mb-1">{{ post.content|striptags|truncate(100) }}</p>
<small>分类: {{ post.category.name }} | 评论: {{ post.comments.count() }}</small>
</a>
{% else %}
<div class="list-group-item">暂无文章</div>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<span>最近评论</span>
<a href="{{ url_for('blog.admin_comments') }}" class="btn btn-sm btn-primary">查看全部</a>
</div>
</div>
<div class="card-body">
<div class="list-group">
{% for comment in recent_comments %}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ comment.author.username }}</h5>
<small>{{ comment.created_time.strftime('%Y-%m-%d %H:%M') }}</small>
</div>
<p class="mb-1">{{ comment.content|truncate(100) }}</p>
<small>文章: <a href="{{ url_for('blog.post_detail', slug=comment.post.slug) }}">{{ comment.post.title }}</a></small>
<div class="mt-2">
<a href="{{ url_for('blog.post_detail', slug=comment.post.slug, _anchor='comment-' + comment.id|string) }}" class="btn btn-sm btn-outline-primary">查看</a>
<button class="btn btn-sm btn-outline-danger delete-comment" data-id="{{ comment.id }}">删除</button>
</div>
</div>
{% else %}
<div class="list-group-item">暂无评论</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">热门文章</div>
<div class="card-body">
<div class="list-group">
{% for post in popular_posts %}
<a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ post.title }}</h5>
<small><i class="bi bi-eye me-1"></i>{{ post.views }}</small>
</div>
<p class="mb-1">{{ post.content|striptags|truncate(100) }}</p>
<small>分类: {{ post.category.name }} | 评论: {{ post.comments.count() }}</small>
</a>
{% else %}
<div class="list-group-item">暂无文章</div>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">系统信息</div>
<div class="card-body">
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>Python 版本</span>
<span class="badge bg-primary">{{ system_info.python_version }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>Flask 版本</span>
<span class="badge bg-primary">{{ system_info.flask_version }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>数据库类型</span>
<span class="badge bg-primary">{{ system_info.database_type }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>操作系统</span>
<span class="badge bg-primary">{{ system_info.os_info }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>服务器时间</span>
<span class="badge bg-primary">{{ now.strftime('%Y-%m-%d %H:%M:%S') }}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// 删除评论的处理
const deleteButtons = document.querySelectorAll('.delete-comment');
deleteButtons.forEach(button => {
button.addEventListener('click', function() {
const commentId = this.getAttribute('data-id');
if (confirm('确定要删除这条评论吗?')) {
fetch(`/api/comments/${commentId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
}
})
.then(response => {
if (response.ok) {
// 删除成功,刷新页面
window.location.reload();
} else {
alert('删除评论失败');
}
})
.catch(error => {
console.error('Error:', error);
alert('删除评论时发生错误');
});
}
});
});
});
</script>
{% endblock %}
{% endblock %}
app\templates\admin\posts.html
text/html
复制代码
{% extends "base.html" %}
{% block title %}文章管理 - Flask个人博客系统{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>文章管理</h1>
<a href="{{ url_for('blog.create_post') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> 新建文章
</a>
</div>
{% if posts %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>标题</th>
<th>分类</th>
<th>发布状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for post in posts %}
<tr>
<td>{{ post.id }}</td>
<td>
<a href="{{ url_for('blog.post_detail', slug=post.slug) }}" target="_blank">
{{ post.title }}
</a>
</td>
<td>{{ post.category.name }}</td>
<td>
{% if post.published %}
<span class="badge bg-success">已发布</span>
{% else %}
<span class="badge bg-secondary">草稿</span>
{% endif %}
</td>
<td>{{ post.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<div class="btn-group" role="group">
<a href="{{ url_for('blog.edit_post', post_id=post.id) }}" class="btn btn-sm btn-outline-primary">
编辑
</a>
<button type="button" class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal" data-bs-target="#deleteModal{{ post.id }}">
删除
</button>
</div>
<!-- Delete Modal -->
<div class="modal fade" id="deleteModal{{ post.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">确认删除</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>确定要删除文章 "{{ post.title }}" 吗?此操作不可撤销。</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<form action="{{ url_for('blog.delete_post', post_id=post.id) }}" method="post">
<button type="submit" class="btn btn-danger">确认删除</button>
</form>
</div>
</div>
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">暂无文章,点击上方"新建文章"按钮创建第一篇文章。</div>
{% endif %}
{% endblock %}
app\templates\admin\post_form.html
text/html
复制代码
{% extends "base.html" %}
{% block title %}
{% if post %}编辑文章{% else %}新建文章{% endif %} - Flask个人博客系统
{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css">
<style>
.CodeMirror, .CodeMirror-scroll {
min-height: 300px;
}
.select2-container {
width: 100% !important;
}
</style>
{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h1 class="h3 mb-0">{% if post %}编辑文章{% else %}新建文章{% endif %}</h1>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
<div class="mb-3">
<label for="title" class="form-label">标题 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="title" name="title"
value="{{ post.title if post else '' }}" required>
</div>
<div class="mb-3">
<label for="category_id" class="form-label">分类 <span class="text-danger">*</span></label>
<select class="form-select" id="category_id" name="category_id" required>
<option value="">选择分类</option>
{% for category in categories %}
<option value="{{ category.id }}"
{% if post and post.category_id == category.id %}selected{% endif %}>
{{ category.name }}
</option>
{% endfor %}
</select>
<div class="form-text">
没有合适的分类?<a href="{{ url_for('blog.create_category') }}" target="_blank">创建新分类</a>
</div>
</div>
<div class="mb-3">
<label for="tags" class="form-label">标签</label>
<input type="text" class="form-control" id="tags" name="tags"
value="{{ post_tags if post_tags else '' }}"
placeholder="输入标签,用逗号分隔">
<div class="form-text">
多个标签用英文逗号分隔,例如:Python,Flask,Web开发
</div>
</div>
<div class="mb-3">
<label for="summary" class="form-label">摘要</label>
<textarea class="form-control" id="summary" name="summary" rows="3">{{ post.summary if post else '' }}</textarea>
<div class="form-text">
如果不填写摘要,将自动截取正文前200个字符作为摘要
</div>
</div>
<div class="mb-3">
<label for="content" class="form-label">正文内容 <span class="text-danger">*</span></label>
<textarea class="form-control" id="content" name="content" rows="10" required>{{ post.content if post else '' }}</textarea>
<div class="form-text">
支持Markdown格式
</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="published" name="published"
{% if post and post.published %}checked{% endif %}>
<label class="form-check-label" for="published">立即发布</label>
<div class="form-text">
如果不勾选,文章将保存为草稿状态
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('blog.admin_posts') }}" class="btn btn-secondary">取消</a>
<button type="submit" class="btn btn-primary">
{% if post %}更新文章{% else %}发布文章{% endif %}
</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Markdown editor
const easyMDE = new EasyMDE({
element: document.getElementById('content'),
spellChecker: false,
autosave: {
enabled: true,
uniqueId: 'blog-post-content',
delay: 1000,
},
toolbar: [
'bold', 'italic', 'heading', '|',
'quote', 'unordered-list', 'ordered-list', '|',
'link', 'image', 'code', 'table', '|',
'preview', 'side-by-side', 'fullscreen', '|',
'guide'
]
});
// Initialize Select2 for tags
$('#tags').select2({
tags: true,
tokenSeparators: [','],
placeholder: '输入标签,用逗号分隔'
});
});
</script>
{% endblock %}
text/html
复制代码
{% extends "base.html" %}
{% block title %}标签管理 - Flask个人博客系统{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>标签管理</h1>
<a href="{{ url_for('blog.create_tag') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> 新建标签
</a>
</div>
<div class="card">
<div class="card-header bg-white">
<div class="row align-items-center">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input type="text" class="form-control" id="tagSearch" placeholder="搜索标签...">
</div>
</div>
<div class="col-md-6 text-md-end mt-3 mt-md-0">
<span class="badge bg-primary">总计: {{ tags|length }} 个标签</span>
</div>
</div>
</div>
<div class="card-body">
{% if tags %}
<div class="mb-4">
<div class="row align-items-center">
<div class="col-auto">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="selectAllTags">
<label class="form-check-label" for="selectAllTags">全选</label>
</div>
</div>
<div class="col">
<select class="form-select form-select-sm" id="bulkAction">
<option value="">批量操作...</option>
<option value="delete">删除所选标签</option>
<option value="merge">合并所选标签</option>
</select>
</div>
<div class="col-auto">
<button class="btn btn-sm btn-secondary" id="applyBulkAction">应用</button>
</div>
</div>
</div>
<div class="tag-cloud">
{% for tag in tags %}
<div class="tag-item">
<div class="form-check form-check-inline">
<input class="form-check-input tag-checkbox" type="checkbox" value="{{ tag.id }}" id="tag{{ tag.id }}">
<label class="form-check-label" for="tag{{ tag.id }}"></label>
</div>
<span class="tag-name">{{ tag.name }}</span>
<span class="tag-count">{{ tag.posts.count() }}</span>
<div class="btn-group btn-group-sm ms-2">
<a href="{{ url_for('blog.tag', name=tag.name) }}" target="_blank"
class="btn btn-sm btn-outline-secondary" data-bs-toggle="tooltip" title="查看标签">
<i class="fas fa-eye"></i>
</a>
<a href="{{ url_for('blog.edit_tag', tag_id=tag.id) }}"
class="btn btn-sm btn-outline-primary" data-bs-toggle="tooltip" title="编辑标签">
<i class="fas fa-edit"></i>
</a>
<button type="button" class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal" data-bs-target="#deleteModal{{ tag.id }}"
data-bs-toggle="tooltip" title="删除标签">
<i class="fas fa-trash"></i>
</button>
</div>
<!-- Delete Modal -->
<div class="modal fade" id="deleteModal{{ tag.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">确认删除</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>确定要删除标签 "{{ tag.name }}" 吗?此操作不可撤销。</p>
{% if tag.posts.count() > 0 %}
<div class="alert alert-warning">
此标签关联了 {{ tag.posts.count() }} 篇文章,删除后这些文章将不再拥有此标签。
</div>
{% endif %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<form action="{{ url_for('blog.delete_tag', tag_id=tag.id) }}" method="post">
<button type="submit" class="btn btn-danger">确认删除</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">暂无标签,点击上方"新建标签"按钮创建第一个标签。</div>
{% endif %}
</div>
{% if tags|length > 20 %}
<div class="card-footer">
<div class="text-center">
<a href="{{ url_for('blog.create_tag') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> 新建标签
</a>
</div>
</div>
{% endif %}
</div>
<div class="card mt-4">
<div class="card-header">标签管理指南</div>
<div class="card-body">
<h5>关于标签</h5>
<p>标签是一种灵活的方式来组织和分类您的内容,一篇文章可以有多个标签。</p>
<h5>管理提示</h5>
<ul>
<li>使用简洁明了的标签名称,避免过长或含有特殊字符</li>
<li>保持标签的一致性,例如使用单数形式而非复数形式</li>
<li>定期整理标签,合并相似的标签以保持标签系统的整洁</li>
<li>删除标签不会删除关联的文章,只会移除文章与标签之间的关联</li>
</ul>
<h5>批量操作</h5>
<p>您可以通过选中多个标签并使用批量操作功能来:</p>
<ul>
<li><strong>删除多个标签</strong>:一次性删除多个标签</li>
<li><strong>合并标签</strong>:将多个相似的标签合并为一个新标签</li>
</ul>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
{% endblock %}
text/html
复制代码
{% extends "base.html" %}
{% block title %}
{% if tag %}编辑标签{% else %}新建标签{% endif %} - Flask个人博客系统
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h1 class="h3 mb-0">{% if tag %}编辑标签{% else %}新建标签{% endif %}</h1>
</div>
<div class="card-body">
<form method="post" class="needs-validation" novalidate>
<div class="mb-3">
<label for="name" class="form-label">标签名称 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="name" name="name"
value="{{ tag.name if tag else '' }}" required
minlength="2" maxlength="64" pattern="[^\s,]+"
placeholder="输入标签名称,不含空格或逗号">
<div class="invalid-feedback">
标签名称是必填项,且不能包含空格或逗号
</div>
<div class="form-text">
标签名称应简洁明了,避免使用空格或特殊字符
</div>
</div>
{% if not tag %}
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="createMultiple" name="createMultiple">
<label class="form-check-label" for="createMultiple">
批量创建多个标签
</label>
</div>
<div id="multipleTagsSection" class="mt-2 d-none">
<label for="multipleNames" class="form-label">多个标签名称 <span class="text-danger">*</span></label>
<textarea class="form-control" id="multipleNames" name="multipleNames" rows="3"
placeholder="输入多个标签,用逗号分隔"></textarea>
<div class="form-text">
输入多个标签名称,用逗号分隔,例如:技术,编程,Flask
</div>
</div>
</div>
{% endif %}
{% if tag %}
<div class="mb-3">
<label class="form-label">使用情况</label>
<p class="form-control-static">
此标签已被用于 <strong>{{ tag.posts.count() }}</strong> 篇文章
</p>
</div>
<div class="mb-3">
<label for="mergeTo" class="form-label">合并到其他标签</label>
<select class="form-select" id="mergeTo" name="mergeTo">
<option value="">-- 不合并 --</option>
{% for other_tag in tags %}
{% if other_tag.id != tag.id %}
<option value="{{ other_tag.id }}">{{ other_tag.name }}</option>
{% endif %}
{% endfor %}
</select>
<div class="form-text">
选择一个标签以将当前标签合并到该标签,所有关联的文章将被重新关联到新标签
</div>
</div>
{% endif %}
<div class="d-flex justify-content-between">
<a href="{{ url_for('blog.admin_tags') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> 返回标签列表
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> {% if tag %}更新标签{% else %}创建标签{% endif %}
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">标签指南</div>
<div class="card-body">
<h5>创建有效的标签</h5>
<ul>
<li>使用简洁明了的名称</li>
<li>避免使用空格,可以使用连字符或下划线</li>
<li>保持标签的一致性,例如使用单数形式</li>
<li>避免创建过于相似的标签</li>
</ul>
<h5>标签管理最佳实践</h5>
<ul>
<li>定期检查和整理标签系统</li>
<li>合并相似的标签以保持系统整洁</li>
<li>删除很少使用的标签</li>
<li>为常用标签创建描述性名称</li>
</ul>
</div>
</div>
{% if tag and tag.posts.count() > 0 %}
<div class="card mt-3">
<div class="card-header">相关文章</div>
<div class="card-body">
<p>此标签下有 <strong>{{ tag.posts.count() }}</strong> 篇文章</p>
<a href="{{ url_for('blog.tag', name=tag.name) }}" class="btn btn-outline-primary btn-sm" target="_blank">
<i class="fas fa-external-link-alt"></i> 查看标签页面
</a>
</div>
</div>
{% endif %}
<div class="card mt-3">
<div class="card-header">热门标签</div>
<div class="card-body">
<div class="popular-tags">
{% for popular_tag in popular_tags %}
<a href="{{ url_for('blog.tag', name=popular_tag.name) }}" class="badge bg-primary me-1 mb-1">
{{ popular_tag.name }} ({{ popular_tag.posts.count() }})
</a>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
<script>
// 表单验证
(function() {
'use strict';
// 获取所有需要验证的表单
var forms = document.querySelectorAll('.needs-validation');
// 循环并阻止提交
Array.prototype.slice.call(forms).forEach(function(form) {
form.addEventListener('submit', function(event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
});
// 批量创建标签切换
const createMultipleCheckbox = document.getElementById('createMultiple');
const multipleTagsSection = document.getElementById('multipleTagsSection');
const nameInput = document.getElementById('name');
const multipleNamesInput = document.getElementById('multipleNames');
if (createMultipleCheckbox && multipleTagsSection) {
createMultipleCheckbox.addEventListener('change', function() {
if (this.checked) {
multipleTagsSection.classList.remove('d-none');
nameInput.required = false;
multipleNamesInput.required = true;
} else {
multipleTagsSection.classList.add('d-none');
nameInput.required = true;
multipleNamesInput.required = false;
}
});
}
// 标签名称实时验证
if (nameInput) {
nameInput.addEventListener('input', function() {
// 移除空格和逗号
this.value = this.value.replace(/[\s,]/g, '');
});
}
})();
</script>
{% endblock %}
app\templates\auth\change_password.html
text/html
复制代码
{% extends 'base.html' %}
{% block title %}修改密码 - Flask个人博客系统{% endblock %}
{% block content %}
<div class="container py-4">
<div class="row">
<div class="col-md-3">
<div class="card mb-4">
<div class="card-body text-center">
<div class="mb-3">
{% if current_user.avatar %}
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="img-fluid rounded-circle" style="width: 150px; height: 150px; object-fit: cover;">
{% else %}
<img src="/static/images/default-avatar.png" alt="{{ current_user.username }}" class="img-fluid rounded-circle" style="width: 150px; height: 150px; object-fit: cover;">
{% endif %}
</div>
<h5 class="card-title">{{ current_user.username }}</h5>
<p class="text-muted">{{ current_user.email }}</p>
<p class="text-muted">
<small>注册于: {{ current_user.created_time.strftime('%Y-%m-%d') }}</small>
</p>
<p class="text-muted">
<small>上次登录: {{ current_user.last_seen.strftime('%Y-%m-%d %H:%M') }}</small>
</p>
</div>
</div>
<div class="list-group mb-4">
<a href="{{ url_for('auth.profile') }}" class="list-group-item list-group-item-action">
<i class="bi bi-person-fill me-2"></i> 个人资料
</a>
<a href="{{ url_for('auth.my_posts') }}" class="list-group-item list-group-item-action">
<i class="bi bi-file-text-fill me-2"></i> 我的文章
</a>
<a href="{{ url_for('auth.change_password') }}" class="list-group-item list-group-item-action active">
<i class="bi bi-key-fill me-2"></i> 修改密码
</a>
{% if current_user.is_admin %}
<a href="{{ url_for('auth.user_list') }}" class="list-group-item list-group-item-action">
<i class="bi bi-people-fill me-2"></i> 用户管理
</a>
<a href="{{ url_for('blog.admin') }}" class="list-group-item list-group-item-action">
<i class="bi bi-speedometer2 me-2"></i> 管理仪表盘
</a>
{% endif %}
</div>
</div>
<div class="col-md-9">
<div class="card">
<div class="card-header">
<h5 class="mb-0">修改密码</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('auth.change_password') }}" class="needs-validation" novalidate>
<div class="mb-3">
<label for="current_password" class="form-label">当前密码</label>
<input type="password" class="form-control" id="current_password" name="current_password" required>
</div>
<div class="mb-3">
<label for="new_password" class="form-label">新密码</label>
<input type="password" class="form-control" id="new_password" name="new_password" required minlength="6">
<div class="form-text">密码长度至少为6个字符</div>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">确认新密码</label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
</div>
<button type="submit" class="btn btn-primary">更新密码</button>
</form>
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<h5 class="mb-0">密码安全提示</h5>
</div>
<div class="card-body">
<ul class="mb-0">
<li>使用至少8个字符的密码</li>
<li>包含大小写字母、数字和特殊字符</li>
<li>避免使用容易猜到的信息,如生日、姓名等</li>
<li>定期更换密码以提高安全性</li>
<li>不要在多个网站使用相同的密码</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{% block scripts %}
<script>
// 表单验证
(function() {
'use strict';
window.addEventListener('load', function() {
var forms = document.getElementsByClassName('needs-validation');
var validation = Array.prototype.filter.call(forms, function(form) {
form.addEventListener('submit', function(event) {
if (form.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
}
// 检查密码是否匹配
var newPassword = document.getElementById('new_password');
var confirmPassword = document.getElementById('confirm_password');
if (newPassword.value !== confirmPassword.value) {
confirmPassword.setCustomValidity('两次输入的密码不匹配');
event.preventDefault();
event.stopPropagation();
} else {
confirmPassword.setCustomValidity('');
}
form.classList.add('was-validated');
}, false);
});
}, false);
})();
</script>
{% endblock %}
{% endblock %}
app\templates\auth\login.html
text/html
复制代码
{% extends 'base.html' %}
{% block title %}登录{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="mb-0">用户登录</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('auth.login') }}">
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="remember" name="remember">
<label class="form-check-label" for="remember">记住我</label>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">登录</button>
</div>
</form>
</div>
<div class="card-footer text-center">
<p class="mb-0">还没有账号? <a href="{{ url_for('auth.register') }}">立即注册</a></p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
app\templates\auth\my_posts.html
text/html
复制代码
{% extends 'base.html' %}
{% block title %}我的文章 - Flask个人博客系统{% endblock %}
{% block content %}
<div class="container py-4">
<div class="row">
<div class="col-md-3">
<div class="card mb-4">
<div class="card-body text-center">
<div class="mb-3">
{% if current_user.avatar %}
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="img-fluid rounded-circle" style="width: 150px; height: 150px; object-fit: cover;">
{% else %}
<img src="/static/images/default-avatar.png" alt="{{ current_user.username }}" class="img-fluid rounded-circle" style="width: 150px; height: 150px; object-fit: cover;">
{% endif %}
</div>
<h5 class="card-title">{{ current_user.username }}</h5>
<p class="text-muted">{{ current_user.email }}</p>
<p class="text-muted">
<small>注册于: {{ current_user.created_time.strftime('%Y-%m-%d') }}</small>
</p>
<p class="text-muted">
<small>上次登录: {{ current_user.last_seen.strftime('%Y-%m-%d %H:%M') }}</small>
</p>
</div>
</div>
<div class="list-group mb-4">
<a href="{{ url_for('auth.profile') }}" class="list-group-item list-group-item-action">
<i class="bi bi-person-fill me-2"></i> 个人资料
</a>
<a href="{{ url_for('auth.my_posts') }}" class="list-group-item list-group-item-action active">
<i class="bi bi-file-text-fill me-2"></i> 我的文章
</a>
<a href="{{ url_for('auth.change_password') }}" class="list-group-item list-group-item-action">
<i class="bi bi-key-fill me-2"></i> 修改密码
</a>
{% if current_user.is_admin %}
<a href="{{ url_for('auth.user_list') }}" class="list-group-item list-group-item-action">
<i class="bi bi-people-fill me-2"></i> 用户管理
</a>
{% endif %}
</div>
</div>
<div class="col-md-9">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">我的文章</h5>
<a href="{{ url_for('blog.create_post') }}" class="btn btn-sm btn-primary">
<i class="bi bi-plus-lg"></i> 写新文章
</a>
</div>
<div class="card-body">
{% if posts.items %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>标题</th>
<th>分类</th>
<th>发布时间</th>
<th>浏览</th>
<th>评论</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for post in posts.items %}
<tr>
<td>
<a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="text-decoration-none">{{ post.title }}</a>
</td>
<td>
<span class="badge bg-primary">{{ post.category.name }}</span>
</td>
<td>{{ post.created_time.strftime('%Y-%m-%d') }}</td>
<td>{{ post.views }}</td>
<td>{{ post.comments.count() }}</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{{ url_for('blog.edit_post', id=post.id) }}" class="btn btn-outline-primary">
<i class="bi bi-pencil"></i>
</a>
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteModal{{ post.id }}">
<i class="bi bi-trash"></i>
</button>
</div>
<!-- 删除确认模态框 -->
<div class="modal fade" id="deleteModal{{ post.id }}" tabindex="-1" aria-labelledby="deleteModalLabel{{ post.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel{{ post.id }}">确认删除</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
确定要删除文章 "{{ post.title }}" 吗?此操作不可撤销。
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<a href="{{ url_for('blog.delete_post', id=post.id) }}" class="btn btn-danger">确认删除</a>
</div>
</div>
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- 分页 -->
{% if posts.pages > 1 %}
<nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center">
{% if posts.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('auth.my_posts', page=posts.prev_num) }}" aria-label="Previous">
<span aria-hidden="true"><<</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true"><<</span>
</a>
</li>
{% endif %}
{% for page_num in posts.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
{% if page_num %}
{% if page_num == posts.page %}
<li class="page-item active"><a class="page-link" href="#">{{ page_num }}</a></li>
{% else %}
<li class="page-item"><a class="page-link" href="{{ url_for('auth.my_posts', page=page_num) }}">{{ page_num }}</a></li>
{% endif %}
{% else %}
<li class="page-item disabled"><a class="page-link" href="#">...</a></li>
{% endif %}
{% endfor %}
{% if posts.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('auth.my_posts', page=posts.next_num) }}" aria-label="Next">
<span aria-hidden="true">>></span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">>></span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="alert alert-info">
<p class="mb-0">您还没有发布任何文章</p>
<div class="mt-3">
<a href="{{ url_for('blog.create_post') }}" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> 写第一篇文章
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
app\templates\auth\profile.html
text/html
复制代码
{% extends 'base.html' %}
{% block title %}个人资料 - Flask个人博客系统{% endblock %}
{% block content %}
<div class="container py-4">
<div class="row">
<div class="col-md-3">
<div class="card mb-4">
<div class="card-body text-center">
<div class="mb-3">
{% if current_user.avatar %}
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="img-fluid rounded-circle" style="width: 150px; height: 150px; object-fit: cover;">
{% else %}
<img src="/static/images/default-avatar.png" alt="{{ current_user.username }}" class="img-fluid rounded-circle" style="width: 150px; height: 150px; object-fit: cover;">
{% endif %}
</div>
<h5 class="card-title">{{ current_user.username }}</h5>
<p class="text-muted">{{ current_user.email }}</p>
<p class="text-muted">
<small>注册于: {{ current_user.created_time.strftime('%Y-%m-%d') }}</small>
</p>
<p class="text-muted">
<small>上次登录: {{ current_user.last_seen.strftime('%Y-%m-%d %H:%M') }}</small>
</p>
</div>
</div>
<div class="list-group mb-4">
<a href="{{ url_for('auth.profile') }}" class="list-group-item list-group-item-action active">
<i class="bi bi-person-fill me-2"></i> 个人资料
</a>
<a href="{{ url_for('auth.my_posts') }}" class="list-group-item list-group-item-action">
<i class="bi bi-file-text-fill me-2"></i> 我的文章
</a>
<a href="{{ url_for('auth.change_password') }}" class="list-group-item list-group-item-action">
<i class="bi bi-key-fill me-2"></i> 修改密码
</a>
{% if current_user.is_admin %}
<a href="{{ url_for('auth.user_list') }}" class="list-group-item list-group-item-action">
<i class="bi bi-people-fill me-2"></i> 用户管理
</a>
<a href="{{ url_for('blog.admin') }}" class="list-group-item list-group-item-action">
<i class="bi bi-speedometer2 me-2"></i> 管理仪表盘
</a>
{% endif %}
</div>
</div>
<div class="col-md-9">
<div class="card">
<div class="card-header">
<h5 class="mb-0">编辑个人资料</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('auth.profile') }}" enctype="multipart/form-data">
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" id="username" name="username" value="{{ current_user.username }}" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">电子邮箱</label>
<input type="email" class="form-control" id="email" name="email" value="{{ current_user.email }}" required>
</div>
<div class="mb-3">
<label for="avatar" class="form-label">头像</label>
<input type="file" class="form-control" id="avatar" name="avatar" accept="image/*">
<div class="form-text">支持JPG、PNG格式,建议上传正方形图片</div>
</div>
<div class="mb-3">
<label for="about_me" class="form-label">关于我</label>
<textarea class="form-control" id="about_me" name="about_me" rows="5">{{ current_user.about_me or '' }}</textarea>
</div>
<button type="submit" class="btn btn-primary">保存更改</button>
</form>
</div>
</div>
{% if current_user.about_me %}
<div class="card mt-4">
<div class="card-header">
<h5 class="mb-0">关于我</h5>
</div>
<div class="card-body">
<p>{{ current_user.about_me }}</p>
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
app\templates\auth\register.html
text/html
复制代码
{% extends 'base.html' %}
{% block title %}注册{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="mb-0">用户注册</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('auth.register') }}" class="needs-validation" novalidate>
<div class="mb-3">
<label for="username" class="form-label">用户名 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="username" name="username" required>
<div class="form-text">用户名将显示在您发布的文章和评论中</div>
</div>
<div class="mb-3">
<label for="email" class="form-label">电子邮箱 <span class="text-danger">*</span></label>
<input type="email" class="form-control" id="email" name="email" required>
<div class="form-text">我们不会向任何人分享您的邮箱</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">密码 <span class="text-danger">*</span></label>
<input type="password" class="form-control" id="password" name="password" required minlength="6">
<div class="form-text">密码长度至少为6个字符</div>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">确认密码 <span class="text-danger">*</span></label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">注册</button>
</div>
</form>
</div>
<div class="card-footer text-center">
<p class="mb-0">已有账号? <a href="{{ url_for('auth.login') }}">立即登录</a></p>
</div>
</div>
</div>
</div>
</div>
{% block scripts %}
<script>
// 表单验证
(function() {
'use strict';
window.addEventListener('load', function() {
var forms = document.getElementsByClassName('needs-validation');
var validation = Array.prototype.filter.call(forms, function(form) {
form.addEventListener('submit', function(event) {
if (form.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
}
// 检查密码是否匹配
var password = document.getElementById('password');
var confirmPassword = document.getElementById('confirm_password');
if (password.value !== confirmPassword.value) {
confirmPassword.setCustomValidity('两次输入的密码不匹配');
event.preventDefault();
event.stopPropagation();
} else {
confirmPassword.setCustomValidity('');
}
form.classList.add('was-validated');
}, false);
});
}, false);
})();
</script>
{% endblock %}
{% endblock %}
app\templates\auth\user_list.html
text/html
复制代码
{% extends 'base.html' %}
{% block title %}用户管理 - Flask个人博客系统{% endblock %}
{% block content %}
<div class="container py-4">
<div class="row">
<div class="col-md-3">
<div class="card mb-4">
<div class="card-body text-center">
<div class="mb-3">
{% if current_user.avatar %}
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="img-fluid rounded-circle" style="width: 150px; height: 150px; object-fit: cover;">
{% else %}
<img src="/static/images/default-avatar.png" alt="{{ current_user.username }}" class="img-fluid rounded-circle" style="width: 150px; height: 150px; object-fit: cover;">
{% endif %}
</div>
<h5 class="card-title">{{ current_user.username }}</h5>
<p class="text-muted">{{ current_user.email }}</p>
<p class="text-muted">
<small>注册于: {{ current_user.created_time.strftime('%Y-%m-%d') }}</small>
</p>
<p class="text-muted">
<small>上次登录: {{ current_user.last_seen.strftime('%Y-%m-%d %H:%M') }}</small>
</p>
</div>
</div>
<div class="list-group mb-4">
<a href="{{ url_for('auth.profile') }}" class="list-group-item list-group-item-action">
<i class="bi bi-person-fill me-2"></i> 个人资料
</a>
<a href="{{ url_for('auth.my_posts') }}" class="list-group-item list-group-item-action">
<i class="bi bi-file-text-fill me-2"></i> 我的文章
</a>
<a href="{{ url_for('auth.change_password') }}" class="list-group-item list-group-item-action">
<i class="bi bi-key-fill me-2"></i> 修改密码
</a>
{% if current_user.is_admin %}
<a href="{{ url_for('auth.user_list') }}" class="list-group-item list-group-item-action active">
<i class="bi bi-people-fill me-2"></i> 用户管理
</a>
<a href="{{ url_for('blog.admin') }}" class="list-group-item list-group-item-action">
<i class="bi bi-speedometer2 me-2"></i> 管理仪表盘
</a>
{% endif %}
</div>
</div>
<div class="col-md-9">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">用户管理</h5>
<div class="input-group" style="width: 300px;">
<input type="text" id="userSearchInput" class="form-control" placeholder="搜索用户...">
<button class="btn btn-outline-secondary" type="button" id="clearSearch">
<i class="bi bi-x"></i>
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>注册时间</th>
<th>角色</th>
<th>操作</th>
</tr>
</thead>
<tbody id="userTableBody">
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>
<div class="d-flex align-items-center">
{% if user.avatar %}
<img src="{{ user.avatar }}" alt="{{ user.username }}" class="rounded-circle me-2" style="width: 32px; height: 32px; object-fit: cover;">
{% else %}
<img src="/static/images/default-avatar.png" alt="{{ user.username }}" class="rounded-circle me-2" style="width: 32px; height: 32px; object-fit: cover;">
{% endif %}
{{ user.username }}
</div>
</td>
<td>{{ user.email }}</td>
<td>{{ user.created_time.strftime('%Y-%m-%d') }}</td>
<td>
{% if user.is_admin %}
<span class="badge bg-primary">管理员</span>
{% else %}
<span class="badge bg-secondary">普通用户</span>
{% endif %}
</td>
<td>
{% if user.id != current_user.id %}
<div class="btn-group">
<form action="{{ url_for('auth.toggle_admin', user_id=user.id) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-primary me-1" title="{{ '取消管理员' if user.is_admin else '设为管理员' }}">
<i class="bi {{ 'bi-person-dash' if user.is_admin else 'bi-person-plus' }}"></i>
</button>
</form>
<button type="button" class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteUserModal{{ user.id }}" title="删除用户">
<i class="bi bi-trash"></i>
</button>
</div>
<!-- 删除用户确认模态框 -->
<div class="modal fade" id="deleteUserModal{{ user.id }}" tabindex="-1" aria-labelledby="deleteUserModalLabel{{ user.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteUserModalLabel{{ user.id }}">确认删除</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>您确定要删除用户 <strong>{{ user.username }}</strong> 吗?</p>
<p class="text-danger">此操作不可逆,用户的所有数据将被删除。</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<form action="{{ url_for('auth.delete_user', user_id=user.id) }}" method="POST">
<button type="submit" class="btn btn-danger">确认删除</button>
</form>
</div>
</div>
</div>
</div>
{% else %}
<span class="text-muted">当前用户</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if not users %}
<div class="text-center py-4">
<p class="text-muted">暂无用户数据</p>
</div>
{% endif %}
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<h5 class="mb-0">用户统计</h5>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-md-4">
<div class="border rounded p-3 mb-3">
<h3>{{ users|length }}</h3>
<p class="text-muted mb-0">总用户数</p>
</div>
</div>
<div class="col-md-4">
<div class="border rounded p-3 mb-3">
<h3>{{ users|selectattr('is_admin', 'eq', true)|list|length }}</h3>
<p class="text-muted mb-0">管理员数</p>
</div>
</div>
<div class="col-md-4">
<div class="border rounded p-3 mb-3">
<h3>{{ users|selectattr('is_admin', 'ne', true)|list|length }}</h3>
<p class="text-muted mb-0">普通用户数</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% block scripts %}
<script>
// 用户搜索功能
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('userSearchInput');
const clearButton = document.getElementById('clearSearch');
const tableBody = document.getElementById('userTableBody');
const rows = tableBody.querySelectorAll('tr');
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
rows.forEach(row => {
const username = row.querySelector('td:nth-child(2)').textContent.toLowerCase();
const email = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
if (username.includes(searchTerm) || email.includes(searchTerm)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
clearButton.addEventListener('click', function() {
searchInput.value = '';
rows.forEach(row => {
row.style.display = '';
});
});
});
</script>
{% endblock %}
{% endblock %}
app\templates\blog\archives.html
text/html
复制代码
{% extends "base.html" %}
{% from "blog/sidebar.html" import render_sidebar %}
{% block title %}文章归档 - Flask个人博客系统{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8">
<div class="mb-4">
<h2>文章归档</h2>
<p class="text-muted">共 {{ post_count }} 篇文章</p>
</div>
{% if archives %}
{% for year, months in archives.items() %}
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">{{ year }}年</h3>
</div>
<div class="card-body">
{% for month, posts in months.items() %}
<div class="mb-4">
<h4 class="border-bottom pb-2">{{ month }}月 ({{ posts|length }}篇)</h4>
<div class="list-group">
{% for post in posts %}
<a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ post.title }}</h5>
<small class="text-muted">{{ post.created_time.strftime('%Y-%m-%d') }}</small>
</div>
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="badge bg-primary me-1">{{ post.category.name }}</span>
{% for tag in post.tags %}
<span class="badge bg-secondary me-1">{{ tag.name }}</span>
{% endfor %}
</div>
<div class="text-muted small">
<i class="bi bi-eye me-1"></i>{{ post.views }}
<i class="bi bi-chat-dots ms-2 me-1"></i>{{ post.comments.count() }}
</div>
</div>
</a>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% else %}
<div class="alert alert-info">
<p class="mb-0">暂无文章</p>
</div>
{% endif %}
</div>
<!-- 使用侧边栏组件 -->
{{ render_sidebar(categories, tags, recent_posts=recent_posts, post_count=post_count, comment_count=comment_count) }}
</div>
</div>
{% endblock %}
app\templates\blog\category.html
text/html
复制代码
{% extends "base.html" %}
{% from "blog/sidebar.html" import render_sidebar %}
{% block title %}分类: {{ category.name }} - Flask个人博客系统{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8">
<div class="mb-4">
<h2>分类: {{ category.name }}</h2>
<p class="text-muted">共 {{ posts.total }} 篇文章</p>
</div>
{% if posts.items %}
{% for post in posts.items %}
<div class="card mb-4">
<div class="card-body">
<h3 class="card-title">
<a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="text-decoration-none">
{{ post.title }}
</a>
</h3>
<div class="card-subtitle mb-2 text-muted small">
<i class="bi bi-calendar me-1"></i>{{ post.created_time.strftime('%Y-%m-%d') }} |
<i class="bi bi-folder me-1"></i><a href="{{ url_for('blog.category', name=post.category.name) }}" class="text-decoration-none">{{ post.category.name }}</a> |
<i class="bi bi-person me-1"></i>{{ post.author.username }}
</div>
<p class="card-text">{{ post.content|striptags|truncate(200) }}</p>
<div class="mb-2">
{% for tag in post.tags %}
<a href="{{ url_for('blog.tag', name=tag.name) }}" class="badge bg-secondary text-decoration-none">{{ tag.name }}</a>
{% endfor %}
</div>
<div class="d-flex justify-content-between align-items-center">
<a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="btn btn-sm btn-primary">
阅读全文 <i class="bi bi-arrow-right"></i>
</a>
<div class="text-muted small">
<i class="bi bi-eye me-1"></i>{{ post.views }}
<i class="bi bi-chat-dots ms-2 me-1"></i>{{ post.comments.count() }}
</div>
</div>
</div>
</div>
{% endfor %}
<!-- 分页 -->
{% if posts.pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if posts.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('blog.category', name=category.name, page=posts.prev_num) }}" aria-label="Previous">
<span aria-hidden="true"><<</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true"><<</span>
</a>
</li>
{% endif %}
{% for page_num in posts.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
{% if page_num %}
{% if page_num == posts.page %}
<li class="page-item active"><a class="page-link" href="#">{{ page_num }}</a></li>
{% else %}
<li class="page-item"><a class="page-link" href="{{ url_for('blog.category', name=category.name, page=page_num) }}">{{ page_num }}</a></li>
{% endif %}
{% else %}
<li class="page-item disabled"><a class="page-link" href="#">...</a></li>
{% endif %}
{% endfor %}
{% if posts.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('blog.category', name=category.name, page=posts.next_num) }}" aria-label="Next">
<span aria-hidden="true">>></span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">>></span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="alert alert-info">
<p class="mb-0">该分类下暂无文章</p>
</div>
{% endif %}
</div>
<!-- 使用侧边栏组件 -->
{{ render_sidebar(categories, tags, recent_posts=recent_posts, post_count=post_count, comment_count=comment_count) }}
</div>
</div>
{% endblock %}
text/html
复制代码
{% macro render_comments(post, comments) %}
<div class="comments-section mt-5" id="comments-section">
<h4 class="mb-4">评论 ({{ comments|length }})</h4>
{% if current_user.is_authenticated %}
<div class="card mb-4">
<div class="card-body">
<form id="comment-form" data-post-id="{{ post.id }}">
<div class="mb-3">
<textarea class="form-control" id="comment-content" rows="3" placeholder="写下您的评论..." required></textarea>
</div>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary">发表评论</button>
</div>
</form>
</div>
</div>
{% else %}
<div class="alert alert-info mb-4">
请 <a href="{{ url_for('auth.login') }}">登录</a> 后发表评论
</div>
{% endif %}
<div id="comments-container">
{% if comments %}
{% for comment in comments if not comment.parent %}
{{ render_comment(comment) }}
{% endfor %}
{% else %}
<div class="text-center py-4" id="no-comments-message">
<p class="text-muted">暂无评论,快来发表第一条评论吧!</p>
</div>
{% endif %}
</div>
</div>
{% endmacro %}
{% macro render_comment(comment) %}
<div class="comment card mb-3" id="comment-{{ comment.id }}">
<div class="card-body">
<div class="d-flex">
<div class="flex-shrink-0">
{% if comment.author.avatar %}
<img src="{{ comment.author.avatar }}" alt="{{ comment.author.username }}" class="rounded-circle" width="50" height="50" style="object-fit: cover;">
{% else %}
<img src="/static/images/default-avatar.png" alt="{{ comment.author.username }}" class="rounded-circle" width="50" height="50">
{% endif %}
</div>
<div class="flex-grow-1 ms-3">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0">{{ comment.author.username }}</h6>
<small class="text-muted">{{ comment.created_time.strftime('%Y-%m-%d %H:%M') }}</small>
</div>
<div class="comment-content mt-2">
<p>{{ comment.content }}</p>
</div>
<div class="comment-actions mt-2">
{% if current_user.is_authenticated %}
<button class="btn btn-sm btn-link p-0 reply-button" data-comment-id="{{ comment.id }}">回复</button>
{% if current_user.id == comment.author.id or current_user.is_admin %}
<button class="btn btn-sm btn-link p-0 ms-2 edit-button" data-comment-id="{{ comment.id }}">编辑</button>
<button class="btn btn-sm btn-link p-0 ms-2 text-danger delete-button" data-comment-id="{{ comment.id }}">删除</button>
{% endif %}
{% endif %}
</div>
<!-- 回复表单,默认隐藏 -->
{% if current_user.is_authenticated %}
<div class="reply-form mt-3" id="reply-form-{{ comment.id }}" style="display: none;">
<form class="reply-comment-form" data-parent-id="{{ comment.id }}" data-post-id="{{ comment.post.id }}">
<div class="mb-3">
<textarea class="form-control reply-content" rows="2" placeholder="回复 {{ comment.author.username }}..." required></textarea>
</div>
<div class="d-flex justify-content-end">
<button type="button" class="btn btn-sm btn-secondary me-2 cancel-reply">取消</button>
<button type="submit" class="btn btn-sm btn-primary">回复</button>
</div>
</form>
</div>
<!-- 编辑表单,默认隐藏 -->
{% if current_user.id == comment.author.id or current_user.is_admin %}
<div class="edit-form mt-3" id="edit-form-{{ comment.id }}" style="display: none;">
<form class="edit-comment-form" data-comment-id="{{ comment.id }}">
<div class="mb-3">
<textarea class="form-control edit-content" rows="2" required>{{ comment.content }}</textarea>
</div>
<div class="d-flex justify-content-end">
<button type="button" class="btn btn-sm btn-secondary me-2 cancel-edit">取消</button>
<button type="submit" class="btn btn-sm btn-primary">保存</button>
</div>
</form>
</div>
{% endif %}
{% endif %}
<!-- 嵌套回复 -->
{% if comment.replies.count() > 0 %}
<div class="nested-comments mt-3">
{% for reply in comment.replies %}
<div class="nested-comment card mb-2" id="comment-{{ reply.id }}">
<div class="card-body py-2">
<div class="d-flex">
<div class="flex-shrink-0">
{% if reply.author.avatar %}
<img src="{{ reply.author.avatar }}" alt="{{ reply.author.username }}" class="rounded-circle" width="35" height="35" style="object-fit: cover;">
{% else %}
<img src="/static/images/default-avatar.png" alt="{{ reply.author.username }}" class="rounded-circle" width="35" height="35">
{% endif %}
</div>
<div class="flex-grow-1 ms-2">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0 fs-6">{{ reply.author.username }}</h6>
<small class="text-muted">{{ reply.created_time.strftime('%Y-%m-%d %H:%M') }}</small>
</div>
<div class="comment-content mt-1">
<p class="mb-1">{{ reply.content }}</p>
</div>
<div class="comment-actions">
{% if current_user.is_authenticated %}
<button class="btn btn-sm btn-link p-0 reply-button" data-comment-id="{{ comment.id }}">回复</button>
{% if current_user.id == reply.author.id or current_user.is_admin %}
<button class="btn btn-sm btn-link p-0 ms-2 edit-button" data-comment-id="{{ reply.id }}">编辑</button>
<button class="btn btn-sm btn-link p-0 ms-2 text-danger delete-button" data-comment-id="{{ reply.id }}">删除</button>
{% endif %}
{% endif %}
</div>
<!-- 编辑表单,默认隐藏 -->
{% if current_user.is_authenticated and (current_user.id == reply.author.id or current_user.is_admin) %}
<div class="edit-form mt-2" id="edit-form-{{ reply.id }}" style="display: none;">
<form class="edit-comment-form" data-comment-id="{{ reply.id }}">
<div class="mb-2">
<textarea class="form-control edit-content" rows="2" required>{{ reply.content }}</textarea>
</div>
<div class="d-flex justify-content-end">
<button type="button" class="btn btn-sm btn-secondary me-2 cancel-edit">取消</button>
<button type="submit" class="btn btn-sm btn-primary">保存</button>
</div>
</form>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endmacro %}
app\templates\blog\create_post.html
text/html
复制代码
{% extends 'base.html' %}
{% block title %}创建文章 - Flask个人博客系统{% endblock %}
{% block styles %}
<!-- 引入Summernote编辑器 -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/summernote-bs4.min.css" rel="stylesheet">
<style>
.note-editor .dropdown-toggle::after {
display: none;
}
</style>
{% endblock %}
{% block content %}
<div class="container py-4">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">创建新文章</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('blog.create_post') }}" enctype="multipart/form-data">
<div class="mb-3">
<label for="title" class="form-label">标题</label>
<input type="text" class="form-control" id="title" name="title" required>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="category" class="form-label">分类</label>
<select class="form-select" id="category" name="category_id" required>
<option value="" selected disabled>选择分类</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label for="tags" class="form-label">标签</label>
<select class="form-select" id="tags" name="tags" multiple data-placeholder="选择标签">
{% for tag in tags %}
<option value="{{ tag.id }}">{{ tag.name }}</option>
{% endfor %}
</select>
<div class="form-text">按住Ctrl键可以选择多个标签</div>
</div>
</div>
<div class="mb-3">
<label for="cover_image" class="form-label">封面图片</label>
<input type="file" class="form-control" id="cover_image" name="cover_image" accept="image/*">
<div class="form-text">可选,支持JPG、PNG格式</div>
</div>
<div class="mb-3">
<label for="content" class="form-label">内容</label>
<textarea class="form-control" id="content" name="content" rows="15" required></textarea>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="published" name="published" checked>
<label class="form-check-label" for="published">
立即发布
</label>
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('auth.my_posts') }}" class="btn btn-secondary">取消</a>
<div>
<button type="submit" name="save_draft" value="1" class="btn btn-outline-primary me-2">保存草稿</button>
<button type="submit" class="btn btn-primary">发布文章</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<!-- 引入Summernote编辑器 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/summernote-bs4.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/lang/summernote-zh-CN.min.js"></script>
<script>
$(document).ready(function() {
$('#content').summernote({
placeholder: '在这里输入文章内容...',
height: 400,
lang: 'zh-CN',
toolbar: [
['style', ['style']],
['font', ['bold', 'underline', 'clear']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture', 'video']],
['view', ['fullscreen', 'codeview', 'help']]
],
callbacks: {
onImageUpload: function(files) {
// 这里可以实现图片上传功能
for (let i = 0; i < files.length; i++) {
uploadImage(files[i], this);
}
}
}
});
// 图片上传函数
function uploadImage(file, editor) {
const formData = new FormData();
formData.append('file', file);
$.ajax({
url: '{{ url_for("blog.upload_image") }}',
method: 'POST',
data: formData,
contentType: false,
processData: false,
success: function(data) {
if (data.success) {
$(editor).summernote('insertImage', data.url, function($image) {
$image.css('max-width', '100%');
});
} else {
alert('图片上传失败: ' + data.message);
}
},
error: function() {
alert('图片上传失败,请稍后重试');
}
});
}
});
</script>
{% endblock %}
app\templates\blog\edit_post.html
text/html
复制代码
{% extends 'base.html' %}
{% block title %}编辑文章 - Flask个人博客系统{% endblock %}
{% block styles %}
<!-- 引入Summernote编辑器 -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/summernote-bs4.min.css" rel="stylesheet">
<style>
.note-editor .dropdown-toggle::after {
display: none;
}
</style>
{% endblock %}
{% block content %}
<div class="container py-4">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">编辑文章</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('blog.edit_post', id=post.id) }}" enctype="multipart/form-data">
<div class="mb-3">
<label for="title" class="form-label">标题</label>
<input type="text" class="form-control" id="title" name="title" value="{{ post.title }}" required>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="category" class="form-label">分类</label>
<select class="form-select" id="category" name="category_id" required>
<option value="" disabled>选择分类</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if category.id == post.category_id %}selected{% endif %}>{{ category.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label for="tags" class="form-label">标签</label>
<select class="form-select" id="tags" name="tags" multiple data-placeholder="选择标签">
{% for tag in tags %}
<option value="{{ tag.id }}" {% if tag in post.tags %}selected{% endif %}>{{ tag.name }}</option>
{% endfor %}
</select>
<div class="form-text">按住Ctrl键可以选择多个标签</div>
</div>
</div>
<div class="mb-3">
<label for="cover_image" class="form-label">封面图片</label>
{% if post.cover_image %}
<div class="mb-2">
<img src="{{ post.cover_image }}" alt="当前封面图" class="img-thumbnail" style="max-height: 200px;">
<div class="form-text">当前封面图片</div>
</div>
{% endif %}
<input type="file" class="form-control" id="cover_image" name="cover_image" accept="image/*">
<div class="form-text">可选,支持JPG、PNG格式。如果不上传新图片,将保留原有封面图</div>
</div>
<div class="mb-3">
<label for="content" class="form-label">内容</label>
<textarea class="form-control" id="content" name="content" rows="15" required>{{ post.content }}</textarea>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="published" name="published" {% if post.published %}checked{% endif %}>
<label class="form-check-label" for="published">
发布
</label>
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('auth.my_posts') }}" class="btn btn-secondary">取消</a>
<div>
<button type="submit" name="save_draft" value="1" class="btn btn-outline-primary me-2">保存草稿</button>
<button type="submit" class="btn btn-primary">更新文章</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<!-- 引入Summernote编辑器 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/summernote-bs4.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/lang/summernote-zh-CN.min.js"></script>
<script>
$(document).ready(function() {
$('#content').summernote({
placeholder: '在这里输入文章内容...',
height: 400,
lang: 'zh-CN',
toolbar: [
['style', ['style']],
['font', ['bold', 'underline', 'clear']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture', 'video']],
['view', ['fullscreen', 'codeview', 'help']]
],
callbacks: {
onImageUpload: function(files) {
// 这里可以实现图片上传功能
for (let i = 0; i < files.length; i++) {
uploadImage(files[i], this);
}
}
}
});
// 图片上传函数
function uploadImage(file, editor) {
const formData = new FormData();
formData.append('file', file);
$.ajax({
url: '{{ url_for("blog.upload_image") }}',
method: 'POST',
data: formData,
contentType: false,
processData: false,
success: function(data) {
if (data.success) {
$(editor).summernote('insertImage', data.url, function($image) {
$image.css('max-width', '100%');
});
} else {
alert('图片上传失败: ' + data.message);
}
},
error: function() {
alert('图片上传失败,请稍后重试');
}
});
}
});
</script>
{% endblock %}
app\templates\blog\index.html
text/html
复制代码
{% extends "base.html" %}
{% from "blog/sidebar.html" import render_sidebar %}
{% block title %}首页 - Flask个人博客系统{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8">
<h2 class="mb-4">最新文章</h2>
{% if posts.items %}
{% for post in posts.items %}
<div class="card mb-4">
<div class="card-body">
<h3 class="card-title">
<a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="text-decoration-none">
{{ post.title }}
</a>
</h3>
<div class="card-subtitle mb-2 text-muted small">
<i class="bi bi-calendar me-1"></i>{{ post.created_time.strftime('%Y-%m-%d') }} |
<i class="bi bi-folder me-1"></i><a href="{{ url_for('blog.category', name=post.category.name) }}" class="text-decoration-none">{{ post.category.name }}</a> |
<i class="bi bi-person me-1"></i>{{ post.author.username }}
</div>
<p class="card-text">{{ post.content|striptags|truncate(200) }}</p>
<div class="mb-2">
{% for tag in post.tags %}
<a href="{{ url_for('blog.tag', name=tag.name) }}" class="badge bg-secondary text-decoration-none">{{ tag.name }}</a>
{% endfor %}
</div>
<div class="d-flex justify-content-between align-items-center">
<a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="btn btn-sm btn-primary">
阅读全文 <i class="bi bi-arrow-right"></i>
</a>
<div class="text-muted small">
<i class="bi bi-eye me-1"></i>{{ post.views }}
<i class="bi bi-chat-dots ms-2 me-1"></i>{{ post.comments.count() }}
</div>
</div>
</div>
</div>
{% endfor %}
<!-- 分页 -->
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if posts.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('blog.index', page=posts.prev_num) }}" aria-label="Previous">
<span aria-hidden="true"><<</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true"><<</span>
</a>
</li>
{% endif %}
{% for page_num in posts.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
{% if page_num %}
{% if page_num == posts.page %}
<li class="page-item active"><a class="page-link" href="#">{{ page_num }}</a></li>
{% else %}
<li class="page-item"><a class="page-link" href="{{ url_for('blog.index', page=page_num) }}">{{ page_num }}</a></li>
{% endif %}
{% else %}
<li class="page-item disabled"><a class="page-link" href="#">...</a></li>
{% endif %}
{% endfor %}
{% if posts.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('blog.index', page=posts.next_num) }}" aria-label="Next">
<span aria-hidden="true">>></span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">>></span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% else %}
<div class="alert alert-info">
<p class="mb-0">暂无文章</p>
</div>
{% endif %}
</div>
<!-- 使用侧边栏组件 -->
{{ render_sidebar(categories, tags, recent_posts=recent_posts, post_count=post_count, comment_count=comment_count) }}
</div>
</div>
{% endblock %}
app\templates\blog\post_detail.html
text/html
复制代码
{% extends "base.html" %}
{% from "blog/sidebar.html" import render_sidebar %}
{% block title %}{{ post.title }} - Flask个人博客系统{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/themes/prism.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
<style>
.blog-post-content img {
max-width: 100%;
height: auto;
}
.comment-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
}
.comment-form {
margin-bottom: 2rem;
}
.comment-item {
margin-bottom: 1.5rem;
}
.comment-reply {
margin-left: 3rem;
}
.comment-actions {
font-size: 0.8rem;
}
</style>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-8">
<article class="blog-post">
<h1 class="mb-3">{{ post.title }}</h1>
<div class="mb-3 text-muted">
<small>
发布于: {{ post.created_time.strftime('%Y-%m-%d %H:%M') }} |
分类: <a href="{{ url_for('blog.category', name=post.category.name) }}">{{ post.category.name }}</a> |
作者: {{ post.author.username }}
{% if post.updated_time and post.updated_time != post.created_time %}
| 更新于: {{ post.updated_time.strftime('%Y-%m-%d %H:%M') }}
{% endif %}
</small>
</div>
<div class="mb-3">
{% for tag in post.tags %}
<a href="{{ url_for('blog.tag', name=tag.name) }}" class="badge bg-secondary text-decoration-none">{{ tag.name }}</a>
{% endfor %}
</div>
<div class="blog-post-content">
{{ post.content|safe }}
</div>
</article>
<hr class="my-5">
<!-- Comments Section -->
{% from 'blog/comments.html' import render_comments %}
{{ render_comments(post, comments) }}
</div>
<div class="col-md-4">
{{ render_sidebar() }}
<div class="card mb-4">
<div class="card-header">相关文章</div>
<div class="card-body">
<ul class="list-unstyled">
{% for related_post in related_posts %}
<li class="mb-2">
<a href="{{ url_for('blog.post_detail', slug=related_post.slug) }}">{{ related_post.title }}</a>
</li>
{% else %}
<li>暂无相关文章</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/prism.min.js"></script>
<script src="{{ url_for('static', filename='js/comments.js') }}"></script>
{% endblock %}
app\templates\blog\search.html
text/html
复制代码
{% extends "base.html" %}
{% from "blog/sidebar.html" import render_sidebar %}
{% block title %}搜索结果: {{ query }} - Flask个人博客系统{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8">
<div class="mb-4">
<h2>搜索结果: "{{ query }}"</h2>
<p class="text-muted">共找到 {{ posts.total }} 篇相关文章</p>
</div>
<!-- 搜索表单 -->
<div class="card mb-4">
<div class="card-body">
<form action="{{ url_for('blog.search') }}" method="get" class="d-flex">
<input type="text" name="q" class="form-control me-2" placeholder="搜索文章..." value="{{ query }}" required>
<button type="submit" class="btn btn-primary">
<i class="bi bi-search"></i> 搜索
</button>
</form>
</div>
</div>
{% if posts.items %}
{% for post in posts.items %}
<div class="card mb-4">
<div class="card-body">
<h3 class="card-title">
<a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="text-decoration-none">
{{ post.title|replace(query, '<mark>' + query + '</mark>')|safe if query in post.title else post.title }}
</a>
</h3>
<div class="card-subtitle mb-2 text-muted small">
<i class="bi bi-calendar me-1"></i>{{ post.created_time.strftime('%Y-%m-%d') }} |
<i class="bi bi-folder me-1"></i><a href="{{ url_for('blog.category', name=post.category.name) }}" class="text-decoration-none">{{ post.category.name }}</a> |
<i class="bi bi-person me-1"></i>{{ post.author.username }}
</div>
<p class="card-text">
{% set content_preview = post.content|striptags|truncate(200) %}
{{ content_preview|replace(query, '<mark>' + query + '</mark>')|safe if query in content_preview else content_preview }}
</p>
<div class="mb-2">
{% for tag in post.tags %}
<a href="{{ url_for('blog.tag', name=tag.name) }}" class="badge bg-secondary text-decoration-none">{{ tag.name }}</a>
{% endfor %}
</div>
<div class="d-flex justify-content-between align-items-center">
<a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="btn btn-sm btn-primary">
阅读全文 <i class="bi bi-arrow-right"></i>
</a>
<div class="text-muted small">
<i class="bi bi-eye me-1"></i>{{ post.views }}
<i class="bi bi-chat-dots ms-2 me-1"></i>{{ post.comments.count() }}
</div>
</div>
</div>
</div>
{% endfor %}
<!-- 分页 -->
{% if posts.pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if posts.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('blog.search', q=query, page=posts.prev_num) }}" aria-label="Previous">
<span aria-hidden="true"><<</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true"><<</span>
</a>
</li>
{% endif %}
{% for page_num in posts.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
{% if page_num %}
{% if page_num == posts.page %}
<li class="page-item active"><a class="page-link" href="#">{{ page_num }}</a></li>
{% else %}
<li class="page-item"><a class="page-link" href="{{ url_for('blog.search', q=query, page=page_num) }}">{{ page_num }}</a></li>
{% endif %}
{% else %}
<li class="page-item disabled"><a class="page-link" href="#">...</a></li>
{% endif %}
{% endfor %}
{% if posts.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('blog.search', q=query, page=posts.next_num) }}" aria-label="Next">
<span aria-hidden="true">>></span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">>></span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="alert alert-info">
<p class="mb-0">未找到与 "{{ query }}" 相关的文章</p>
<p class="mt-2">搜索建议:</p>
<ul>
<li>确保所有单词拼写正确</li>
<li>尝试使用更通用的关键词</li>
<li>尝试使用更少的关键词</li>
</ul>
</div>
{% endif %}
</div>
<!-- 使用侧边栏组件 -->
{{ render_sidebar(categories, tags, recent_posts=recent_posts, post_count=post_count, comment_count=comment_count) }}
</div>
</div>
{% endblock %}
text/html
复制代码
{% macro render_sidebar(categories, tags, recent_posts=None) %}
<div class="col-md-4">
<!-- 搜索框 -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-search me-2"></i>搜索
</div>
<div class="card-body">
<form action="{{ url_for('blog.search') }}" method="get">
<div class="input-group">
<input type="text" class="form-control" name="q" placeholder="搜索文章..." required>
<button class="btn btn-primary" type="submit">
<i class="bi bi-search"></i>
</button>
</div>
</form>
</div>
</div>
<!-- 分类列表 -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-folder me-2"></i>分类
</div>
<div class="card-body">
<ul class="list-group list-group-flush">
{% for category in categories %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<a href="{{ url_for('blog.category', name=category.name) }}" class="text-decoration-none">
{{ category.name }}
</a>
<span class="badge bg-primary rounded-pill">{{ category.posts.count() }}</span>
</li>
{% else %}
<li class="list-group-item">暂无分类</li>
{% endfor %}
</ul>
</div>
</div>
<!-- 标签云 -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-tags me-2"></i>标签云
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-2">
{% for tag in tags %}
<a href="{{ url_for('blog.tag', name=tag.name) }}" class="text-decoration-none">
<span class="badge bg-secondary fs-6">{{ tag.name }}</span>
</a>
{% else %}
<span>暂无标签</span>
{% endfor %}
</div>
</div>
</div>
<!-- 最近文章 -->
{% if recent_posts %}
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-clock-history me-2"></i>最近文章
</div>
<div class="card-body">
<ul class="list-group list-group-flush">
{% for post in recent_posts %}
<li class="list-group-item">
<a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="text-decoration-none">
{{ post.title }}
</a>
<div class="small text-muted mt-1">
<i class="bi bi-calendar me-1"></i>{{ post.created_time.strftime('%Y-%m-%d') }}
</div>
</li>
{% else %}
<li class="list-group-item">暂无文章</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
<!-- 博客统计 -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-bar-chart me-2"></i>统计信息
</div>
<div class="card-body">
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
文章总数
<span class="badge bg-primary rounded-pill">{{ post_count }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
评论总数
<span class="badge bg-primary rounded-pill">{{ comment_count }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
分类总数
<span class="badge bg-primary rounded-pill">{{ categories|length }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
标签总数
<span class="badge bg-primary rounded-pill">{{ tags|length }}</span>
</li>
</ul>
</div>
</div>
</div>
{% endmacro %}
app\templates\blog\tag.html
text/html
复制代码
{% extends "base.html" %}
{% from "blog/sidebar.html" import render_sidebar %}
{% block title %}标签: {{ tag.name }} - Flask个人博客系统{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8">
<div class="mb-4">
<h2>标签: {{ tag.name }}</h2>
<p class="text-muted">共 {{ posts.total }} 篇文章</p>
</div>
{% if posts.items %}
{% for post in posts.items %}
<div class="card mb-4">
<div class="card-body">
<h3 class="card-title">
<a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="text-decoration-none">
{{ post.title }}
</a>
</h3>
<div class="card-subtitle mb-2 text-muted small">
<i class="bi bi-calendar me-1"></i>{{ post.created_time.strftime('%Y-%m-%d') }} |
<i class="bi bi-folder me-1"></i><a href="{{ url_for('blog.category', name=post.category.name) }}" class="text-decoration-none">{{ post.category.name }}</a> |
<i class="bi bi-person me-1"></i>{{ post.author.username }}
</div>
<p class="card-text">{{ post.content|striptags|truncate(200) }}</p>
<div class="mb-2">
{% for post_tag in post.tags %}
<a href="{{ url_for('blog.tag', name=post_tag.name) }}" class="badge bg-secondary text-decoration-none">{{ post_tag.name }}</a>
{% endfor %}
</div>
<div class="d-flex justify-content-between align-items-center">
<a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="btn btn-sm btn-primary">
阅读全文 <i class="bi bi-arrow-right"></i>
</a>
<div class="text-muted small">
<i class="bi bi-eye me-1"></i>{{ post.views }}
<i class="bi bi-chat-dots ms-2 me-1"></i>{{ post.comments.count() }}
</div>
</div>
</div>
</div>
{% endfor %}
<!-- 分页 -->
{% if posts.pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if posts.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('blog.tag', name=tag.name, page=posts.prev_num) }}" aria-label="Previous">
<span aria-hidden="true"><<</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true"><<</span>
</a>
</li>
{% endif %}
{% for page_num in posts.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
{% if page_num %}
{% if page_num == posts.page %}
<li class="page-item active"><a class="page-link" href="#">{{ page_num }}</a></li>
{% else %}
<li class="page-item"><a class="page-link" href="{{ url_for('blog.tag', name=tag.name, page=page_num) }}">{{ page_num }}</a></li>
{% endif %}
{% else %}
<li class="page-item disabled"><a class="page-link" href="#">...</a></li>
{% endif %}
{% endfor %}
{% if posts.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('blog.tag', name=tag.name, page=posts.next_num) }}" aria-label="Next">
<span aria-hidden="true">>></span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">>></span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="alert alert-info">
<p class="mb-0">该标签下暂无文章</p>
</div>
{% endif %}
</div>
<!-- 使用侧边栏组件 -->
{{ render_sidebar(categories, tags, recent_posts=recent_posts, post_count=post_count, comment_count=comment_count) }}
</div>
</div>
{% endblock %}
app\templates\errors\404.html
text/html
复制代码
{% extends 'base.html' %}
{% block title %}页面未找到{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-8 text-center">
<h1 class="display-1 fw-bold">404</h1>
<p class="fs-3">页面未找到</p>
<p class="lead">您访问的页面不存在或已被移除。</p>
<div class="mt-4">
<a href="{{ url_for('blog.index') }}" class="btn btn-primary">返回首页</a>
</div>
</div>
</div>
</div>
{% endblock %}
app\templates\errors\500.html
text/html
复制代码
{% extends 'base.html' %}
{% block title %}服务器错误{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-8 text-center">
<h1 class="display-1 fw-bold">500</h1>
<p class="fs-3">服务器内部错误</p>
<p class="lead">抱歉,服务器遇到了一个错误。我们正在努力修复这个问题。</p>
<div class="mt-4">
<a href="{{ url_for('blog.index') }}" class="btn btn-primary">返回首页</a>
</div>
</div>
</div>
</div>
{% endblock %}
blog.sql
复制代码
-- 博客系统数据库结构
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 用户表
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(80) NOT NULL,
`email` varchar(120) NOT NULL,
`password_hash` varchar(128) NOT NULL,
`avatar` varchar(255) DEFAULT NULL,
`bio` text,
`website` varchar(255) DEFAULT NULL,
`location` varchar(100) DEFAULT NULL,
`is_admin` tinyint(1) DEFAULT '0',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP,
`last_login` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`),
UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- ----------------------------
-- 分类表
-- ----------------------------
DROP TABLE IF EXISTS `category`;
CREATE TABLE `category` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`description` varchar(255) DEFAULT NULL,
`order` int(11) DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章分类表';
-- ----------------------------
-- 标签表
-- ----------------------------
DROP TABLE IF EXISTS `tag`;
CREATE TABLE `tag` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章标签表';
-- ----------------------------
-- 文章表
-- ----------------------------
DROP TABLE IF EXISTS `post`;
CREATE TABLE `post` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(128) NOT NULL,
`slug` varchar(128) NOT NULL,
`content` text NOT NULL,
`summary` text,
`cover_image` varchar(255) DEFAULT NULL,
`views` int(11) DEFAULT '0',
`likes` int(11) DEFAULT '0',
`published` tinyint(1) DEFAULT '1',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_time` datetime DEFAULT NULL,
`author_id` int(11) NOT NULL,
`category_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `slug` (`slug`),
KEY `author_id` (`author_id`),
KEY `category_id` (`category_id`),
KEY `created_time` (`created_time`),
CONSTRAINT `post_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
CONSTRAINT `post_ibfk_2` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章表';
-- ----------------------------
-- 文章标签关联表
-- ----------------------------
DROP TABLE IF EXISTS `post_tag`;
CREATE TABLE `post_tag` (
`post_id` int(11) NOT NULL,
`tag_id` int(11) NOT NULL,
PRIMARY KEY (`post_id`,`tag_id`),
KEY `tag_id` (`tag_id`),
CONSTRAINT `post_tag_ibfk_1` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE CASCADE,
CONSTRAINT `post_tag_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tag` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章标签关联表';
-- ----------------------------
-- 评论表
-- ----------------------------
DROP TABLE IF EXISTS `comment`;
CREATE TABLE `comment` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`content` text NOT NULL,
`created_time` datetime DEFAULT CURRENT_TIMESTAMP,
`post_id` int(11) NOT NULL,
`author_id` int(11) NOT NULL,
`parent_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `post_id` (`post_id`),
KEY `author_id` (`author_id`),
KEY `parent_id` (`parent_id`),
CONSTRAINT `comment_ibfk_1` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE CASCADE,
CONSTRAINT `comment_ibfk_2` FOREIGN KEY (`author_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
CONSTRAINT `comment_ibfk_3` FOREIGN KEY (`parent_id`) REFERENCES `comment` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表';
-- ----------------------------
-- 系统设置表
-- ----------------------------
DROP TABLE IF EXISTS `setting`;
CREATE TABLE `setting` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`key` varchar(50) NOT NULL,
`value` text,
`description` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `key` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统设置表';
-- ----------------------------
-- 页面表
-- ----------------------------
DROP TABLE IF EXISTS `page`;
CREATE TABLE `page` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(128) NOT NULL,
`slug` varchar(128) NOT NULL,
`content` text NOT NULL,
`published` tinyint(1) DEFAULT '1',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_time` datetime DEFAULT NULL,
`author_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `slug` (`slug`),
KEY `author_id` (`author_id`),
CONSTRAINT `page_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='页面表';
-- ----------------------------
-- 导航菜单表
-- ----------------------------
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`url` varchar(255) NOT NULL,
`order` int(11) DEFAULT '0',
`parent_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `parent_id` (`parent_id`),
CONSTRAINT `menu_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `menu` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='导航菜单表';
-- ----------------------------
-- 文件上传表
-- ----------------------------
DROP TABLE IF EXISTS `upload`;
CREATE TABLE `upload` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`filename` varchar(255) NOT NULL,
`original_filename` varchar(255) NOT NULL,
`mime_type` varchar(100) NOT NULL,
`size` int(11) NOT NULL,
`path` varchar(255) NOT NULL,
`created_time` datetime DEFAULT CURRENT_TIMESTAMP,
`user_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
CONSTRAINT `upload_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件上传表';
-- ----------------------------
-- 初始数据
-- ----------------------------
-- 管理员用户 (密码: admin123)
INSERT INTO `user` (`username`, `email`, `password_hash`, `is_admin`, `created_time`) VALUES
('admin', '[email protected]', 'pbkdf2:sha256:150000$nMmI0Hcg$d89f36192a427a34d5c9b4c64daa46b5d87011b761e44f7c28b70e35bc8df6bb', 1, NOW());
-- 测试用户 (密码: password123)
INSERT INTO `user` (`username`, `email`, `password_hash`, `bio`, `created_time`) VALUES
('test', '[email protected]', 'pbkdf2:sha256:150000$GkVCcgbA$a2d78f9b02de3c9c3d2c593b77d50d2a1a7388eaf3693a1347a834eedb1c83e9', '这是一个测试用户', NOW());
-- 分类数据
INSERT INTO `category` (`name`, `description`, `order`) VALUES
('技术', '技术相关文章', 1),
('生活', '生活随笔', 2),
('教程', '各类教程', 3),
('资源', '资源分享', 4),
('其他', '其他分类', 5);
-- 标签数据
INSERT INTO `tag` (`name`) VALUES
('Python'),
('JavaScript'),
('Flask'),
('Web开发'),
('数据库'),
('前端'),
('后端'),
('AI'),
('机器学习'),
('Linux');
-- 系统设置
INSERT INTO `setting` (`key`, `value`, `description`) VALUES
('site_name', '我的博客', '网站名称'),
('site_description', '一个使用Flask开发的博客系统', '网站描述'),
('site_keywords', '博客,Flask,Python,Web开发', '网站关键词'),
('posts_per_page', '10', '每页显示文章数'),
('comments_enabled', '1', '是否开启评论'),
('registration_enabled', '1', '是否开启注册'),
('admin_email', '[email protected]', '管理员邮箱');
-- 导航菜单
INSERT INTO `menu` (`name`, `url`, `order`, `parent_id`) VALUES
('首页', '/', 1, NULL),
('归档', '/archives', 2, NULL),
('分类', '/category/技术', 3, NULL),
('关于', '/page/about', 4, NULL);
-- 示例文章
INSERT INTO `post` (`title`, `slug`, `content`, `summary`, `published`, `created_time`, `author_id`, `category_id`) VALUES
('欢迎使用Flask博客系统', 'welcome-to-flask-blog', '# 欢迎使用Flask博客系统\n\n这是一个使用Flask开发的博客系统,具有以下特点:\n\n- 文章管理\n- 分类和标签\n- 评论系统\n- 用户管理\n- 响应式设计\n\n## 如何使用\n\n1. 注册账号\n2. 登录系统\n3. 开始发布您的文章\n\n祝您使用愉快!', '这是一个使用Flask开发的博客系统,具有文章管理、分类和标签、评论系统、用户管理、响应式设计等特点。', 1, NOW(), 1, 1),
('Flask入门教程', 'flask-tutorial', '# Flask入门教程\n\n## 什么是Flask\n\nFlask是一个轻量级的Python Web框架,易于学习和使用,同时又足够灵活,可以满足各种需求。\n\n## 安装Flask\n\n```python\npip install flask\n```\n\n## 创建第一个Flask应用\n\n```python\nfrom flask import Flask\n\napp = Flask(__name__)\n\[email protected](\"/\")\ndef hello_world():\n return \"<p>Hello, World!</p>\"\n\nif __name__ == \"__main__\":\n app.run(debug=True)\n```\n\n## 路由和视图函数\n\nFlask使用装饰器来定义路由...\n\n## 模板渲染\n\nFlask使用Jinja2作为模板引擎...\n\n## 表单处理\n\n使用Flask-WTF可以方便地处理表单...\n\n## 数据库集成\n\nFlask-SQLAlchemy提供了ORM支持...\n\n## 结语\n\n通过本教程,您已经了解了Flask的基础知识,可以开始构建自己的Web应用了。', 'Flask是一个轻量级的Python Web框架,本教程介绍了Flask的基础知识,包括安装、创建应用、路由、模板、表单和数据库等内容。', 1, NOW(), 1, 3);
-- 文章标签关联
INSERT INTO `post_tag` (`post_id`, `tag_id`) VALUES
(1, 3), -- 欢迎使用Flask博客系统 - Flask
(1, 4), -- 欢迎使用Flask博客系统 - Web开发
(2, 1), -- Flask入门教程 - Python
(2, 3), -- Flask入门教程 - Flask
(2, 4), -- Flask入门教程 - Web开发
(2, 7); -- Flask入门教程 - 后端
-- 示例页面
INSERT INTO `page` (`title`, `slug`, `content`, `published`, `created_time`, `author_id`) VALUES
('关于我们', 'about', '# 关于我们\n\n这是一个使用Flask开发的博客系统,旨在提供一个简洁、高效的博客平台。\n\n## 联系方式\n\n- 邮箱:[email protected]\n- GitHub:https://github.com/yourusername/flask-blog\n\n欢迎提出宝贵意见和建议!', 1, NOW(), 1);
-- 示例评论
INSERT INTO `comment` (`content`, `created_time`, `post_id`, `author_id`) VALUES
('这是一个很棒的博客系统,期待更多功能!', NOW(), 1, 2),
('教程写得非常清晰,对初学者很友好!', NOW(), 2, 2);
SET FOREIGN_KEY_CHECKS = 1;