当前位置: 首页 > news >正文

Python项目-基于Flask的个人博客系统设计与实现(2)

源代码 续

{% 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">&laquo;</span></a></li>{% else %}<li class="page-item disabled"><a class="page-link" href="#" aria-label="Previous"><span aria-hidden="true">&laquo;</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">&raquo;</span></a></li>{% else %}<li class="page-item disabled"><a class="page-link" href="#" aria-label="Next"><span aria-hidden="true">&raquo;</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

{% 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

{% 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

{% 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/select2@4.1.0-rc.0/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/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>document.addEventListener('DOMContentLoaded', function() {// Initialize Markdown editorconst 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 %}

app\templates\admin\tags.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 %}

app\templates\admin\tag_form.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 '' }}" requiredminlength="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

{% 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

{% 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

{% 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">&laquo;</span></a></li>{% else %}<li class="page-item disabled"><a class="page-link" href="#" aria-label="Previous"><span aria-hidden="true">&laquo;</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">&raquo;</span></a></li>{% else %}<li class="page-item disabled"><a class="page-link" href="#" aria-label="Next"><span aria-hidden="true">&raquo;</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

{% 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

{% 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

{% 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

{% 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

{% 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">&laquo;</span></a></li>{% else %}<li class="page-item disabled"><a class="page-link" href="#" aria-label="Previous"><span aria-hidden="true">&laquo;</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">&raquo;</span></a></li>{% else %}<li class="page-item disabled"><a class="page-link" href="#" aria-label="Next"><span aria-hidden="true">&raquo;</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\blog\comments.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

{% extends 'base.html' %}{% block title %}创建文章 - Flask个人博客系统{% endblock %}{% block styles %}
<!-- 引入Summernote编辑器 -->
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/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/summernote@0.8.18/dist/summernote-bs4.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/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

{% extends 'base.html' %}{% block title %}编辑文章 - Flask个人博客系统{% endblock %}{% block styles %}
<!-- 引入Summernote编辑器 -->
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/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/summernote@0.8.18/dist/summernote-bs4.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/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

{% 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">&laquo;</span></a></li>{% else %}<li class="page-item disabled"><a class="page-link" href="#" aria-label="Previous"><span aria-hidden="true">&laquo;</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">&raquo;</span></a></li>{% else %}<li class="page-item disabled"><a class="page-link" href="#" aria-label="Next"><span aria-hidden="true">&raquo;</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

{% 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/prismjs@1.29.0/themes/prism.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/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/prismjs@1.29.0/prism.min.js"></script>
<script src="{{ url_for('static', filename='js/comments.js') }}"></script>
{% endblock %}

app\templates\blog\search.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">&laquo;</span></a></li>{% else %}<li class="page-item disabled"><a class="page-link" href="#" aria-label="Previous"><span aria-hidden="true">&laquo;</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">&raquo;</span></a></li>{% else %}<li class="page-item disabled"><a class="page-link" href="#" aria-label="Next"><span aria-hidden="true">&raquo;</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 %}

app\templates\blog\sidebar.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

{% 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">&laquo;</span></a></li>{% else %}<li class="page-item disabled"><a class="page-link" href="#" aria-label="Previous"><span aria-hidden="true">&laquo;</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">&raquo;</span></a></li>{% else %}<li class="page-item disabled"><a class="page-link" href="#" aria-label="Next"><span aria-hidden="true">&raquo;</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

{% 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

{% 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', 'admin@example.com', 'pbkdf2:sha256:150000$nMmI0Hcg$d89f36192a427a34d5c9b4c64daa46b5d87011b761e44f7c28b70e35bc8df6bb', 1, NOW());-- 测试用户 (密码: password123)
INSERT INTO `user` (`username`, `email`, `password_hash`, `bio`, `created_time`) VALUES
('test', 'test@example.com', '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', 'admin@example.com', '管理员邮箱');-- 导航菜单
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\n@app.route(\"/\")\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- 邮箱:admin@example.com\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;

相关文章:

Python项目-基于Flask的个人博客系统设计与实现(2)

源代码 续 {% 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 h…...

2023第十四届蓝桥杯大赛软件赛省赛C/C++ 大学 B 组(真题题解)(C++/Java题解)

记录刷题的过程、感悟、题解。 希望能帮到&#xff0c;那些与我一同前行的&#xff0c;来自远方的朋友&#x1f609; 大纲&#xff1a; 1、日期统计-&#xff08;解析&#xff09;-暴力dfs&#xff08;&#x1f609;蓝桥专属 2、01串的熵-&#xff08;解析&#xff09;-不要chu…...

前端界面在线excel编辑器 。node编写post接口获取文件流,使用传参替换表格内容展示、前后端一把梭。

首先luckysheet插件是支持在线替换excel内容编辑得但是浏览器无法调用本地文件&#xff0c;如果只是展示&#xff0c;让后端返回文件得二进制文件流就可以了&#xff0c;直接使用luckysheet展示。 这里我们使用xlsx-populate得node简单应用来调用本地文件&#xff0c;自己写一个…...

‌在 Fedora 系统下备份远程 Windows SQL Server 数据库的完整方案

‌一、环境准备与工具安装‌ ‌1. 安装 Microsoft SQL Server 命令行工具‌ Fedora 需安装 mssql-tools 和 ODBC 驱动&#xff1a; # 添加 Microsoft 仓库 sudo curl -o /etc/yum.repos.d/msprod.repo https://packages.microsoft.com/config/rhel/8/prod.repo# 安装工具包 …...

从24GHz到71GHz:Sivers半导体的广泛频率范围5G毫米波产品解析

在5G技术的浪潮中&#xff0c;Sivers半导体推出了创新的毫米波无线产品&#xff0c;为通信行业带来高效、可靠的解决方案。这些产品支持从24GHz到71GHz的频率&#xff0c;覆盖许可与非许可频段&#xff0c;适应高速、低延迟的通信场景。 5G通信频段的一点事儿及Sivers毫米波射频…...

从【抖音安全与信任中心】观察企业如何做算法透明

抖音主动公开算法原理树立行业新标杆&#xff1a; “抖音安全与信任中心”网站&#xff08;95152.douyin.com&#xff09; 1 算法透明的几点准则 需涵盖技术逻辑公开、治理机制可查、用户参与共建等维度。以下是基于抖音案例总结的可行路径&#xff0c;以及几个准则&#xff1…...

html处理Base文件流

处理步骤 从服务返回的字符串中提取文件流数据&#xff0c;可能是Base64或二进制。将数据转换为Blob对象。创建对象URL。创建<a>元素&#xff0c;设置href和download属性。触发点击事件以下载文件。删除缓存数据 代码 // 假设这是从服务返回的Base64字符串&#xff08…...

MySQL内存管理机制详解

目录标题 MySQL内存管理机制详解1. **内存组成与核心组件**2. **RSS与共享内存的关系**3. **OOM问题排查步骤**4. **典型案例** Buffer Pool&#xff08;缓冲池&#xff09; 确实属于共享内存&#xff08;Shared Memory&#xff09;的核心组成部分&#xff1f;1. **Buffer Pool…...

《算法笔记》9.7小节——数据结构专题(2)->堆 问题 C: 合并果子(堆)

题目描述 在一个果园里&#xff0c;多多已经将所有的果子打了下来&#xff0c;而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。 每一次合并&#xff0c;多多可以把两堆果子合并到一起&#xff0c;消耗的体力等于两堆果子的重量之和。可以看出&#xff0c…...

化繁为简解决leetcode第1289题下降路径最小和II

1289.下降路径最小和II 难度&#xff1a;困难 问题描述&#xff1a; 给你一个nxn整数矩阵grid&#xff0c;请你返回非零偏移下降路径数字和的最小值。 非零偏移下降路径定义为&#xff1a;从grid数组中的每一行选择一个数字&#xff0c;且按顺序选出来的数字中&#xff0c;…...

蓝桥杯省模拟赛 数位和

问题描述 只能被 1 和本身整除的数称为质数。 请问在 1 &#xff08;含&#xff09;到 1000000 &#xff08;含&#xff09;中&#xff0c;有多少个质数的各个数位上的数字之和为 2323 。 提示&#xff1a;599 就是这样一个质数&#xff0c;各个数位上的数字之和为 59923 。…...

MySQL和Oracle批量插入SQL差异详解

文章目录 MySQL和Oracle批量插入SQL差异详解1. 基本批量插入语法1.1 MySQL批量插入1.2 Oracle批量插入 2. 带序列的批量插入2.1 MySQL带自增ID的批量插入2.2 Oracle带序列的批量插入 3. 条件批量插入3.1 MySQL条件批量插入3.2 Oracle条件批量插入 MySQL和Oracle批量插入SQL差异…...

YOLOv5配置训练以及华为昇腾910B推理

参考文章&#xff1a; 保姆式yolov5教程&#xff0c;训练你自己的数据集 - 知乎 Windows 10|11下安装mmyolo-0.5.0版本 - 知乎 Ubuntu22.04安装教程&基于华为Ascend AI处理器的om模型atc转换环境安装_ubuntu安装atc工具-CSDN博客嵌入式AI---在华为昇腾推理自己的yolov5目标…...

Visual Studio Code配置自动规范代码格式

目录 前言1. 插件安装2. 配置个性化设置2.1 在左下角点击设置按钮 &#xff0c;点击命令面板&#xff08;或者也可以之间按快捷键CtrlShiftP&#xff09;2.2 在弹出的搜索框输入 settings.json&#xff0c;打开首选项&#xff1a;打开工作区设置&#xff1b;2.3 在settings.jso…...

【网安面经合集】42 道高频 Web 安全面试题全解析(附原理+防御+思路)

对于正在准备 安全岗求职或实习的同学们来说&#xff0c;Web 安全面试题几乎是必问项。 尤其是一些经常出现的考点&#xff0c;比如 SQL 注入、XSS、CSRF、反序列化、逻辑漏洞、WAF 绕过等等&#xff0c;不仅需要你知道“是什么”&#xff0c;还得能“讲清楚原理、分类、修复和…...

论文笔记(七十五)Auto-Encoding Variational Bayes

Auto-Encoding Variational Bayes 文章概括摘要1 引言2 方法2.1 问题场景2.2 变分下界2.3 SGVB估计器与AEVB算法2.4 重参数化技巧 3 示例&#xff1a;变分自编码器&#xff08;Variational Auto-Encoder&#xff09;4 相关工作5 实验6 结论7 未来工作 文章概括 引用&#xff1…...

前端学习记录之HTML

1. 网页 1.1 什么是网页 网站是指在因特网上根据一定的规则&#xff0c;使用HTML等制作的用于展示特定内容相关的网页集合。 网页是网站中的一“页”&#xff0c;通常是HTML格式的文件&#xff0c;它要通过浏览器来阅读 网页是构成网站的基本元素。它通常由图片&#xff0c;…...

程序化广告行业(39/89):广告投放的数据分析与优化秘籍

程序化广告行业&#xff08;39/89&#xff09;&#xff1a;广告投放的数据分析与优化秘籍 在程序化广告的领域中&#xff0c;数据分析与优化调整是实现精准投放、提升广告效果的核心环节。作为一名热衷于探索程序化广告的学习者&#xff0c;我希望通过这篇博客&#xff0c;和大…...

蓝桥杯 01游戏

问题描述 小蓝最近玩上了 01 游戏&#xff0c;这是一款带有二进制思想的棋子游戏。 游戏在一个大小为 N N 的棋盘上进行。棋盘上的每个位置都需要放置一个数字 0 或 1。初始情况下&#xff0c;棋盘上有一部分位置已经放置了固定的数字&#xff0c;玩家不可以更改这些位置。其…...

NoSQL 数据库的适用场景与局限性分析

NoSQL(Not Only SQL)数据库是一类非关系型数据库,通过灵活的数据模型和分布式架构解决传统关系型数据库在扩展性、性能和数据多样性上的瓶颈。以下从技术特性、适用场景、不适用场景及行业实践展开分析: 一、NoSQL数据库的核心技术特性 四大数据模型 文档型:以JSON/BSON格…...

个人网站:基于html、css、js网页开发界面

1、注册 <!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>注册页面</title><link rel&qu…...

嵌入式图像采集与显示系统实战详解:基于V4L2与Framebuffer的实现

在嵌入式Linux开发中&#xff0c;图像采集与显示是非常典型的一类应用场景。本文将基于 ARM9&#xff08;S3C2410&#xff09; 平台&#xff0c;深入讲解如何使用 V4L2 框架从 USB 摄像头采集图像数据&#xff0c;并通过 Framebuffer 接口实时显示到 LCD 屏幕。内容涵盖驱动架构…...

庙算兵棋推演AI开发初探(6-神经网络开发)

碎碎念&#xff1a; 老师让我和同学组队参加10月底截止报名的庙算比赛&#xff0c;我俩走运进了64强&#xff0c;打的过程中发现了一个重要问题——为什么别人总能打我&#xff0c;但是我都看不见&#xff01;就像玩dota被对面英雄莫名其妙单杀了但是他就一直隐身我都不知道怎…...

嵌入式硬件篇---嘉立创PCB绘制

文章目录 前言一、PCB绘制简介1.1绘制步骤1.1.1前期准备1.1.2原理图设计1.1.3原理图转PCB1.1.4PCB布局1.1.5布线1.1.6布线优化和丝印1.1.7制版 1.2原理1.2.1电气连接原理1.2.2信号传输原理1.2.3电源和接地原理 1.3注意事项1.3.1元件封装1.3.2布局规则1.3.3过孔设计1.3.4DRC检查…...

AI与.NET技术实操系列(四):使用 Semantic Kernel 和 DeepSeek 构建AI应用

1. 引言 在人工智能技术飞速发展的今天&#xff0c;大型语言模型&#xff08;Large Language Models, LLMs&#xff09;已成为智能应用开发的核心驱动力。从智能客服到自动化内容生成&#xff0c;LLMs的应用正在深刻改变我们的工作和生活方式。 对于.NET开发者而言&#xff0c;…...

Vue 组件 - Slot 内容分发

Vue 渐进式JavaScript 框架 基于Vue2的学习笔记 - Vue组件 - Slot 内容分发 目录 Slot内容分发 旧版slot 单插槽 使用插槽 具名插槽 插槽实现导航 使用插槽优点 新版slot Or 插槽版抽屉 总结 Slot内容分发 混合父组件的内容和子组件自己模板 -- 内容分发 父组件模…...

Mysql之事务(下)

&#x1f3dd;️专栏&#xff1a;Mysql_猫咪-9527的博客-CSDN博客 &#x1f305;主页&#xff1a;猫咪-9527-CSDN博客 “欲穷千里目&#xff0c;更上一层楼。会当凌绝顶&#xff0c;一览众山小。” 目录 5. 事务的隔离级别与并发控制 5.1事务的隔离级别 5.2查看与设置事务的…...

LabVIEW液压控制系统开发要点

液压控制系统开发需兼顾高实时性、强抗干扰性和安全性&#xff0c;尤其在重工业场景中&#xff0c;毫秒级响应延迟或数据异常都可能导致设备损坏。本文以某钢厂液压升降平台项目为例&#xff0c;从硬件选型、控制算法、安全机制三方面&#xff0c;详解LabVIEW开发中的关键问题与…...

mybatis-genertor(代码生成)源码及扩展笔记

文章目录 生成过程MyBatisGenerator.generate()代码入口 pid0,id0context.generateFiles()代码 pid0,id1introspectedTable.getGeneratedJavaFiles() java部分生成 pid1,id11introspectedTable.getGeneratedXmlFiles() xml部分生成 pid1,id12这里是一波三连调用XMLMapperGenera…...

Mysql-数据库、安装、登录

一. 数据库 1. 数据库&#xff1a;DataBase&#xff08;DB&#xff09;&#xff0c;是存储和管理数据的仓库。 2. 数据库管理系统&#xff1a;DataBase Management System&#xff08;DBMS&#xff09;,操纵管理数据库的大型软件 3. SQL&#xff1a;Structured Query Language&…...

HTTP 请求方法

HTTP 请求方法 引言 HTTP(超文本传输协议)是互联网上应用最为广泛的网络协议之一。它定义了客户端与服务器之间通信的规则。HTTP请求方法,也称为HTTP动词,是客户端向服务器发送请求时使用的操作类型。本文将详细介绍HTTP请求方法的概念、分类、常用方法及其在实际应用中的…...

群体智能优化算法-算术优化算法(Arithmetic Optimization Algorithm, AOA,含Matlab源代码)

摘要 算术优化算法&#xff08;Arithmetic Optimization Algorithm, AOA&#xff09;是一种新颖的群体智能优化算法&#xff0c;灵感来源于加、减、乘、除四种基本算术运算。在优化过程中&#xff0c;AOA 通过乘除操作实现全局探索&#xff0c;通过加减操作强化局部开发&#…...

4.1-python操作wrod/pdf 文件

1.读取word文件 首先安装软件包 pip3 install python-docx from docx import Documentimport os path os.path.join(os.getcwd(),你的文档名字.docx)# 加载文档 doc Document(path)# 遍历数据 for p in doc.paragraphs:print(p.text)# 遍历文档中所有表格 for t in doc.t…...

C# 窗体应用(.FET Framework) 线程操作方法

一、Thread线程使用方法 初始化方法 Thread th1; th1 new Thread(方法名); th1.IsBackground true; th1.Start();传参 ///定义一个object接受参数的方法 private void Test(object n){string str1 n as string; MessageBox.Show(str1); }// 调用方法 Thread th2 string s…...

vscode/cursor编辑器中vue3文件里面的css不能注释解决办法

升级了cursor后发现css或者html里面的代码不能单行注释了&#xff0c;真的很烦人&#xff0c;找了很多解决办法&#xff0c;还是定位到插件上&#xff0c;有一个vue的插件&#xff0c;把它禁用掉就可以注释了&#xff0c;然后再把这个插件启用&#xff0c;就可以使用了&#xf…...

Jenkins详细安装配置部署

Jenkins是一款流行的开源持续集成/持续交付(CI/CD)工具&#xff0c;可以实现自动化构建、测试和部署软件。下面是Jenkins的详细安装、配置和部署过程。 安装Jenkins 1. 安装Java Jenkins运行需要Java环境&#xff0c;因此需要先安装Java。具体安装方式根据不同的操作系统有所…...

《Linux运维总结:基于银河麒麟V10+ARM64架构CPU源码编译部署单实例redis7.2.6》

总结&#xff1a;整理不易&#xff0c;如果对你有帮助&#xff0c;可否点赞关注一下&#xff1f; 更多详细内容请参考&#xff1a;《Linux运维篇&#xff1a;Linux系统运维指南》 一、环境信息 环境信息如下&#xff1a; 主机IP 操作系统 Redis版本 CPU架构 192.168.1.111 K…...

音视频开发---常用工具

一、VLC播放器 1. 简介 VLC多媒体播放器(最初命名为VideoLAN客户端)是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘、VCD影音光盘和各类流式协议。它也能作为unicast或multicast的流式服务器在IPv4或IPv6的高速连接下使用。 它融…...

Java 大视界 -- 基于 Java 的大数据分布式计算在基因测序数据分析中的性能优化(161)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…...

关于跨域与.NET的处理方案

在 Web 开发里&#xff0c;浏览器的同源策略是一项关键的安全机制。同源指的是两个 URL 的协议、域名和端口都相同。当浏览器从一个源&#xff08;域名、协议、端口&#xff09;的网页去请求另一个源的资源时&#xff0c;就会产生跨域问题。例如&#xff0c;从 http://www.exam…...

中级:Maven面试题精讲

一、引言 在Java开发中&#xff0c;Maven作为一款强大的项目管理和构建工具&#xff0c;被广泛应用于项目构建、依赖管理和插件机制等方面。面试官通过相关问题考察候选人对Maven核心功能的理解和实际应用能力&#xff0c;以及在复杂项目场景下合理配置和优化Maven的能力。本文…...

MySQL与Redis数据一致性保障方案详解

前言 在现代分布式系统中&#xff0c;MySQL和Redis的结合使用非常普遍。MySQL作为关系型数据库负责持久化存储&#xff0c;而Redis则作为高性能缓存层提升系统的响应速度。然而&#xff0c;在这种架构下&#xff0c;如何保证MySQL与Redis之间的数据一致性是一个重要的挑战。本…...

外观模式详解

以下是一个结合外观模式解决实际开发问题的Java实现案例&#xff0c;涵盖复杂系统整合、接口简化、版本兼容等场景需求&#xff0c;附带逐行中文注释&#xff1a; 场景描述 开发一个智能家居控制系统&#xff0c;需整合多个子系统&#xff1a; 灯光系统&#xff1a;多房间灯光…...

JavaScript单例模式

单例模式保证一个类仅有一个实例&#xff0c;并提供一个访问它的全局访问点。 用一个变量来标志是否创建过对象&#xff0c;如果是&#xff0c;则在下次直接返回这个已经创建好的对象&#xff0c;示例代码如下&#xff1a; 单例模式的核心思想是让指定的类只存在唯一一个实例&…...

Kong网关研究

目录 概述 部署kong docker服务 kong初始化与启动 验证 部署konga 网关功能 JWT认证 若依的鉴权认证 kong的JWT支持 限流 黑名单 概述 Kong网关基于OpenResty&#xff0c;而OpenResty基于Nginx&#xff0c;Nginx本身是性能强大的方向代理与web容器&#xff0c;Ope…...

LangChain 结构化输出:用 Pydantic + PydanticOutputParser 驯服 LLM 的“自由发挥”

目录 一、Pydantic 二、PydanticOutputParser 1、为什么需要 PydanticOutputParser&#xff1f; 2、Pydantic和PydanticOutputParser核心区别 3、Pydantic的不足 &#xff08;1&#xff09;无法直接解析非结构化文本 &#xff08;2&#xff09;缺乏对 LLM 输出的适配性 …...

source(WEB)

##解题思路 首先打开kali&#xff0c;使用dirsearch扫描下网站目录&#xff0c;发现网站有.git源码泄露 dirsearch -u URL 接着使用wget将源码下载到本地&#xff08;尝试过使用githack&#xff0c;但是得到的信息是flag不在这&#xff09; wget -r URL/.git/ 接着cd到wget的…...

【蓝桥杯】单片机设计与开发,温度传感器DS18B20

一、温度传感器概述 结构图 二、通信过程 三、onewire单总线协议概述 四、单总线的工作原理 黑粗线是单片机发送的&#xff0c;浅的是s18b20回应的 五、温度传感器的应用 六、onewire 七、课后习题...

trae.ai 编辑器:前端开发者的智能效率革命

一、为什么我们需要更智能的编辑器&#xff1f; 作为从业5年的前端开发者&#xff0c;我使用过从Sublime到VSCode的各种编辑器。但随着现代前端技术的复杂度爆炸式增长&#xff08;想想一个React组件可能涉及JSX、CSS-in-JS、TypeScript和GraphQL&#xff09;&#xff0c;传统…...

【FPGA实战】基于DE2-115实现数字秒表

【FPGA实战】基于DE2-115实现数字秒表 一、项目概述二、层次化设计架构三、核心模块实现原理1. 时钟分频模块(clock_divider.v)2. 按键处理模块2.1 消抖(debounce .v)2.2 边沿检测(edge_detector .v) 3. 时间计数模块(time_counter .v)4. 显示驱动模块(seven_seg_display.v)5.顶…...