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

源代码 续

text/html 复制代码
{% extends 'base.html' %}

{% block title %}评论管理{% endblock %}

{% block content %}
<div class="container py-4">
    <div class="row">
        <div class="col-md-3">
            <div class="list-group mb-4">
                <a href="{{ url_for('blog.admin_posts') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-file-earmark-text me-2"></i> 文章管理
                </a>
                <a href="{{ url_for('blog.admin_categories') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-folder me-2"></i> 分类管理
                </a>
                <a href="{{ url_for('blog.admin_tags') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-tags me-2"></i> 标签管理
                </a>
                <a href="{{ url_for('blog.admin_comments') }}" class="list-group-item list-group-item-action active">
                    <i class="bi bi-chat-dots me-2"></i> 评论管理
                </a>
                {% if current_user.is_admin %}
                <a href="{{ url_for('auth.user_list') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-people me-2"></i> 用户管理
                </a>
                {% endif %}
            </div>
        </div>
        
        <div class="col-md-9">
            <div class="card">
                <div class="card-header d-flex justify-content-between align-items-center">
                    <h5 class="mb-0">评论管理</h5>
                    <div class="input-group" style="width: 300px;">
                        <input type="text" id="commentSearchInput" class="form-control" placeholder="搜索评论...">
                        <button class="btn btn-outline-secondary" type="button" id="clearSearch">
                            <i class="bi bi-x"></i>
                        </button>
                    </div>
                </div>
                <div class="card-body">
                    <div class="table-responsive">
                        <table class="table table-hover">
                            <thead>
                                <tr>
                                    <th style="width: 5%">ID</th>
                                    <th style="width: 15%">用户</th>
                                    <th style="width: 15%">文章</th>
                                    <th style="width: 35%">内容</th>
                                    <th style="width: 15%">时间</th>
                                    <th style="width: 15%">操作</th>
                                </tr>
                            </thead>
                            <tbody id="commentTableBody">
                                {% for comment in comments.items %}
                                <tr>
                                    <td>{{ comment.id }}</td>
                                    <td>
                                        <div class="d-flex align-items-center">
                                            {% if comment.author.avatar %}
                                            <img src="{{ comment.author.avatar }}" alt="{{ comment.author.username }}" class="rounded-circle me-2" style="width: 32px; height: 32px; object-fit: cover;">
                                            {% else %}
                                            <img src="/static/images/default-avatar.png" alt="{{ comment.author.username }}" class="rounded-circle me-2" style="width: 32px; height: 32px; object-fit: cover;">
                                            {% endif %}
                                            {{ comment.author.username }}
                                        </div>
                                    </td>
                                    <td>
                                        <a href="{{ url_for('blog.post_detail', slug=comment.post.slug) }}" target="_blank" title="{{ comment.post.title }}">
                                            {{ comment.post.title|truncate(20) }}
                                        </a>
                                    </td>
                                    <td>
                                        {% if comment.parent %}
                                        <span class="badge bg-secondary me-1">回复</span>
                                        {% endif %}
                                        {{ comment.content|truncate(50) }}
                                    </td>
                                    <td>{{ comment.created_time.strftime('%Y-%m-%d %H:%M') }}</td>
                                    <td>
                                        <div class="btn-group">
                                            <a href="{{ url_for('blog.post_detail', slug=comment.post.slug) }}#comment-{{ comment.id }}" class="btn btn-sm btn-outline-primary me-1" title="查看评论">
                                                <i class="bi bi-eye"></i>
                                            </a>
                                            <button type="button" class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteCommentModal{{ comment.id }}" title="删除评论">
                                                <i class="bi bi-trash"></i>
                                            </button>
                                        </div>
                                        
                                        <!-- 删除评论确认模态框 -->
                                        <div class="modal fade" id="deleteCommentModal{{ comment.id }}" tabindex="-1" aria-labelledby="deleteCommentModalLabel{{ comment.id }}" aria-hidden="true">
                                            <div class="modal-dialog">
                                                <div class="modal-content">
                                                    <div class="modal-header">
                                                        <h5 class="modal-title" id="deleteCommentModalLabel{{ comment.id }}">确认删除</h5>
                                                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                                                    </div>
                                                    <div class="modal-body">
                                                        <p>您确定要删除这条评论吗?</p>
                                                        <div class="card">
                                                            <div class="card-body">
                                                                <p class="card-text">{{ comment.content }}</p>
                                                                <p class="card-text"><small class="text-muted">由 {{ comment.author.username }} 发表于 {{ comment.created_time.strftime('%Y-%m-%d %H:%M') }}</small></p>
                                                            </div>
                                                        </div>
                                                    </div>
                                                    <div class="modal-footer">
                                                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
                                                        <form action="{{ url_for('blog.admin_delete_comment', comment_id=comment.id) }}" method="POST">
                                                            <button type="submit" class="btn btn-danger">确认删除</button>
                                                        </form>
                                                    </div>
                                                </div>
                                            </div>
                                        </div>
                                    </td>
                                </tr>
                                {% endfor %}
                            </tbody>
                        </table>
                    </div>
                    
                    {% if not comments.items %}
                    <div class="text-center py-4">
                        <p class="text-muted">暂无评论数据</p>
                    </div>
                    {% endif %}
                    
                    <!-- 分页 -->
                    {% if comments.pages > 1 %}
                    <nav aria-label="Page navigation">
                        <ul class="pagination justify-content-center">
                            {% if comments.has_prev %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('blog.admin_comments', page=comments.prev_num) }}" aria-label="Previous">
                                    <span aria-hidden="true"><<</span>
                                </a>
                            </li>
                            {% else %}
                            <li class="page-item disabled">
                                <a class="page-link" href="#" aria-label="Previous">
                                    <span aria-hidden="true"><<</span>
                                </a>
                            </li>
                            {% endif %}
                            
                            {% for page in comments.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
                                {% if page %}
                                    {% if page == comments.page %}
                                    <li class="page-item active"><a class="page-link" href="#">{{ page }}</a></li>
                                    {% else %}
                                    <li class="page-item"><a class="page-link" href="{{ url_for('blog.admin_comments', page=page) }}">{{ page }}</a></li>
                                    {% endif %}
                                {% else %}
                                <li class="page-item disabled"><a class="page-link" href="#">...</a></li>
                                {% endif %}
                            {% endfor %}
                            
                            {% if comments.has_next %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('blog.admin_comments', page=comments.next_num) }}" aria-label="Next">
                                    <span aria-hidden="true">>></span>
                                </a>
                            </li>
                            {% else %}
                            <li class="page-item disabled">
                                <a class="page-link" href="#" aria-label="Next">
                                    <span aria-hidden="true">>></span>
                                </a>
                            </li>
                            {% endif %}
                        </ul>
                    </nav>
                    {% endif %}
                </div>
            </div>
        </div>
    </div>
</div>

{% block scripts %}
<script>
    // 评论搜索功能
    document.addEventListener('DOMContentLoaded', function() {
        const searchInput = document.getElementById('commentSearchInput');
        const clearButton = document.getElementById('clearSearch');
        const tableBody = document.getElementById('commentTableBody');
        const rows = tableBody.querySelectorAll('tr');
        
        searchInput.addEventListener('input', function() {
            const searchTerm = this.value.toLowerCase();
            
            rows.forEach(row => {
                const username = row.querySelector('td:nth-child(2)').textContent.toLowerCase();
                const postTitle = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
                const content = row.querySelector('td:nth-child(4)').textContent.toLowerCase();
                
                if (username.includes(searchTerm) || postTitle.includes(searchTerm) || content.includes(searchTerm)) {
                    row.style.display = '';
                } else {
                    row.style.display = 'none';
                }
            });
        });
        
        clearButton.addEventListener('click', function() {
            searchInput.value = '';
            rows.forEach(row => {
                row.style.display = '';
            });
        });
    });
</script>
{% endblock %}
{% endblock %}

app\templates\admin\dashboard.html

text/html 复制代码
{% extends 'base.html' %}

{% block title %}管理仪表板{% endblock %}

{% block content %}
<div class="container">
    <h2 class="mb-4">管理仪表板</h2>
    
    <div class="row mb-4">
        <div class="col-md-3">
            <div class="card text-bg-primary mb-3">
                <div class="card-body">
                    <div class="d-flex justify-content-between align-items-center">
                        <div>
                            <h5 class="card-title">文章</h5>
                            <h2 class="card-text">{{ stats.post_count }}</h2>
                        </div>
                        <i class="bi bi-file-earmark-text fs-1"></i>
                    </div>
                    <a href="{{ url_for('blog.admin_posts') }}" class="btn btn-sm btn-light mt-2">管理文章</a>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="card text-bg-success mb-3">
                <div class="card-body">
                    <div class="d-flex justify-content-between align-items-center">
                        <div>
                            <h5 class="card-title">评论</h5>
                            <h2 class="card-text">{{ stats.comment_count }}</h2>
                        </div>
                        <i class="bi bi-chat-dots fs-1"></i>
                    </div>
                    <a href="{{ url_for('blog.admin_comments') }}" class="btn btn-sm btn-light mt-2">管理评论</a>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="card text-bg-info mb-3">
                <div class="card-body">
                    <div class="d-flex justify-content-between align-items-center">
                        <div>
                            <h5 class="card-title">用户</h5>
                            <h2 class="card-text">{{ stats.user_count }}</h2>
                        </div>
                        <i class="bi bi-people fs-1"></i>
                    </div>
                    <a href="{{ url_for('auth.user_list') }}" class="btn btn-sm btn-light mt-2">管理用户</a>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="card text-bg-warning mb-3">
                <div class="card-body">
                    <div class="d-flex justify-content-between align-items-center">
                        <div>
                            <h5 class="card-title">分类</h5>
                            <h2 class="card-text">{{ stats.category_count }}</h2>
                        </div>
                        <i class="bi bi-folder fs-1"></i>
                    </div>
                    <a href="{{ url_for('blog.admin_categories') }}" class="btn btn-sm btn-light mt-2">管理分类</a>
                </div>
            </div>
        </div>
    </div>
    
    <div class="row">
        <div class="col-md-6">
            <div class="card mb-4">
                <div class="card-header">
                    <div class="d-flex justify-content-between align-items-center">
                        <span>最近发布的文章</span>
                        <a href="{{ url_for('blog.admin_posts') }}" class="btn btn-sm btn-primary">查看全部</a>
                    </div>
                </div>
                <div class="card-body">
                    <div class="list-group">
                        {% for post in recent_posts %}
                        <a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="list-group-item list-group-item-action">
                            <div class="d-flex w-100 justify-content-between">
                                <h5 class="mb-1">{{ post.title }}</h5>
                                <small>{{ post.created_time.strftime('%Y-%m-%d') }}</small>
                            </div>
                            <p class="mb-1">{{ post.content|striptags|truncate(100) }}</p>
                            <small>分类: {{ post.category.name }} | 评论: {{ post.comments.count() }}</small>
                        </a>
                        {% else %}
                        <div class="list-group-item">暂无文章</div>
                        {% endfor %}
                    </div>
                </div>
            </div>
        </div>
        
        <div class="col-md-6">
            <div class="card mb-4">
                <div class="card-header">
                    <div class="d-flex justify-content-between align-items-center">
                        <span>最近评论</span>
                        <a href="{{ url_for('blog.admin_comments') }}" class="btn btn-sm btn-primary">查看全部</a>
                    </div>
                </div>
                <div class="card-body">
                    <div class="list-group">
                        {% for comment in recent_comments %}
                        <div class="list-group-item">
                            <div class="d-flex w-100 justify-content-between">
                                <h5 class="mb-1">{{ comment.author.username }}</h5>
                                <small>{{ comment.created_time.strftime('%Y-%m-%d %H:%M') }}</small>
                            </div>
                            <p class="mb-1">{{ comment.content|truncate(100) }}</p>
                            <small>文章: <a href="{{ url_for('blog.post_detail', slug=comment.post.slug) }}">{{ comment.post.title }}</a></small>
                            <div class="mt-2">
                                <a href="{{ url_for('blog.post_detail', slug=comment.post.slug, _anchor='comment-' + comment.id|string) }}" class="btn btn-sm btn-outline-primary">查看</a>
                                <button class="btn btn-sm btn-outline-danger delete-comment" data-id="{{ comment.id }}">删除</button>
                            </div>
                        </div>
                        {% else %}
                        <div class="list-group-item">暂无评论</div>
                        {% endfor %}
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    <div class="row">
        <div class="col-md-6">
            <div class="card mb-4">
                <div class="card-header">热门文章</div>
                <div class="card-body">
                    <div class="list-group">
                        {% for post in popular_posts %}
                        <a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="list-group-item list-group-item-action">
                            <div class="d-flex w-100 justify-content-between">
                                <h5 class="mb-1">{{ post.title }}</h5>
                                <small><i class="bi bi-eye me-1"></i>{{ post.views }}</small>
                            </div>
                            <p class="mb-1">{{ post.content|striptags|truncate(100) }}</p>
                            <small>分类: {{ post.category.name }} | 评论: {{ post.comments.count() }}</small>
                        </a>
                        {% else %}
                        <div class="list-group-item">暂无文章</div>
                        {% endfor %}
                    </div>
                </div>
            </div>
        </div>
        
        <div class="col-md-6">
            <div class="card mb-4">
                <div class="card-header">系统信息</div>
                <div class="card-body">
                    <ul class="list-group list-group-flush">
                        <li class="list-group-item d-flex justify-content-between align-items-center">
                            <span>Python 版本</span>
                            <span class="badge bg-primary">{{ system_info.python_version }}</span>
                        </li>
                        <li class="list-group-item d-flex justify-content-between align-items-center">
                            <span>Flask 版本</span>
                            <span class="badge bg-primary">{{ system_info.flask_version }}</span>
                        </li>
                        <li class="list-group-item d-flex justify-content-between align-items-center">
                            <span>数据库类型</span>
                            <span class="badge bg-primary">{{ system_info.database_type }}</span>
                        </li>
                        <li class="list-group-item d-flex justify-content-between align-items-center">
                            <span>操作系统</span>
                            <span class="badge bg-primary">{{ system_info.os_info }}</span>
                        </li>
                        <li class="list-group-item d-flex justify-content-between align-items-center">
                            <span>服务器时间</span>
                            <span class="badge bg-primary">{{ now.strftime('%Y-%m-%d %H:%M:%S') }}</span>
                        </li>
                    </ul>
                </div>
            </div>
        </div>
    </div>
</div>

{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
    // 删除评论的处理
    const deleteButtons = document.querySelectorAll('.delete-comment');
    deleteButtons.forEach(button => {
        button.addEventListener('click', function() {
            const commentId = this.getAttribute('data-id');
            if (confirm('确定要删除这条评论吗?')) {
                fetch(`/api/comments/${commentId}`, {
                    method: 'DELETE',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRFToken': '{{ csrf_token() }}'
                    }
                })
                .then(response => {
                    if (response.ok) {
                        // 删除成功,刷新页面
                        window.location.reload();
                    } else {
                        alert('删除评论失败');
                    }
                })
                .catch(error => {
                    console.error('Error:', error);
                    alert('删除评论时发生错误');
                });
            }
        });
    });
});
</script>
{% endblock %}
{% endblock %}

app\templates\admin\posts.html

text/html 复制代码
{% extends "base.html" %}

{% block title %}文章管理 - Flask个人博客系统{% endblock %}

{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
    <h1>文章管理</h1>
    <a href="{{ url_for('blog.create_post') }}" class="btn btn-primary">
        <i class="fas fa-plus"></i> 新建文章
    </a>
</div>

{% if posts %}
    <div class="table-responsive">
        <table class="table table-striped table-hover">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>标题</th>
                    <th>分类</th>
                    <th>发布状态</th>
                    <th>创建时间</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody>
                {% for post in posts %}
                <tr>
                    <td>{{ post.id }}</td>
                    <td>
                        <a href="{{ url_for('blog.post_detail', slug=post.slug) }}" target="_blank">
                            {{ post.title }}
                        </a>
                    </td>
                    <td>{{ post.category.name }}</td>
                    <td>
                        {% if post.published %}
                            <span class="badge bg-success">已发布</span>
                        {% else %}
                            <span class="badge bg-secondary">草稿</span>
                        {% endif %}
                    </td>
                    <td>{{ post.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
                    <td>
                        <div class="btn-group" role="group">
                            <a href="{{ url_for('blog.edit_post', post_id=post.id) }}" class="btn btn-sm btn-outline-primary">
                                编辑
                            </a>
                            <button type="button" class="btn btn-sm btn-outline-danger" 
                                    data-bs-toggle="modal" data-bs-target="#deleteModal{{ post.id }}">
                                删除
                            </button>
                        </div>
                        
                        <!-- Delete Modal -->
                        <div class="modal fade" id="deleteModal{{ post.id }}" tabindex="-1" aria-hidden="true">
                            <div class="modal-dialog">
                                <div class="modal-content">
                                    <div class="modal-header">
                                        <h5 class="modal-title">确认删除</h5>
                                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                                    </div>
                                    <div class="modal-body">
                                        <p>确定要删除文章 "{{ post.title }}" 吗?此操作不可撤销。</p>
                                    </div>
                                    <div class="modal-footer">
                                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
                                        <form action="{{ url_for('blog.delete_post', post_id=post.id) }}" method="post">
                                            <button type="submit" class="btn btn-danger">确认删除</button>
                                        </form>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </div>
{% else %}
    <div class="alert alert-info">暂无文章,点击上方"新建文章"按钮创建第一篇文章。</div>
{% endif %}
{% endblock %}

app\templates\admin\post_form.html

text/html 复制代码
{% extends "base.html" %}

{% block title %}
    {% if post %}编辑文章{% else %}新建文章{% endif %} - Flask个人博客系统
{% endblock %}

{% block extra_css %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css">
<style>
    .CodeMirror, .CodeMirror-scroll {
        min-height: 300px;
    }
    .select2-container {
        width: 100% !important;
    }
</style>
{% endblock %}

{% block content %}
<div class="card">
    <div class="card-header">
        <h1 class="h3 mb-0">{% if post %}编辑文章{% else %}新建文章{% endif %}</h1>
    </div>
    <div class="card-body">
        <form method="post" enctype="multipart/form-data">
            <div class="mb-3">
                <label for="title" class="form-label">标题 <span class="text-danger">*</span></label>
                <input type="text" class="form-control" id="title" name="title" 
                       value="{{ post.title if post else '' }}" required>
            </div>
            
            <div class="mb-3">
                <label for="category_id" class="form-label">分类 <span class="text-danger">*</span></label>
                <select class="form-select" id="category_id" name="category_id" required>
                    <option value="">选择分类</option>
                    {% for category in categories %}
                        <option value="{{ category.id }}" 
                                {% if post and post.category_id == category.id %}selected{% endif %}>
                            {{ category.name }}
                        </option>
                    {% endfor %}
                </select>
                <div class="form-text">
                    没有合适的分类?<a href="{{ url_for('blog.create_category') }}" target="_blank">创建新分类</a>
                </div>
            </div>
            
            <div class="mb-3">
                <label for="tags" class="form-label">标签</label>
                <input type="text" class="form-control" id="tags" name="tags" 
                       value="{{ post_tags if post_tags else '' }}" 
                       placeholder="输入标签,用逗号分隔">
                <div class="form-text">
                    多个标签用英文逗号分隔,例如:Python,Flask,Web开发
                </div>
            </div>
            
            <div class="mb-3">
                <label for="summary" class="form-label">摘要</label>
                <textarea class="form-control" id="summary" name="summary" rows="3">{{ post.summary if post else '' }}</textarea>
                <div class="form-text">
                    如果不填写摘要,将自动截取正文前200个字符作为摘要
                </div>
            </div>
            
            <div class="mb-3">
                <label for="content" class="form-label">正文内容 <span class="text-danger">*</span></label>
                <textarea class="form-control" id="content" name="content" rows="10" required>{{ post.content if post else '' }}</textarea>
                <div class="form-text">
                    支持Markdown格式
                </div>
            </div>
            
            <div class="mb-3 form-check">
                <input type="checkbox" class="form-check-input" id="published" name="published" 
                       {% if post and post.published %}checked{% endif %}>
                <label class="form-check-label" for="published">立即发布</label>
                <div class="form-text">
                    如果不勾选,文章将保存为草稿状态
                </div>
            </div>
            
            <div class="d-flex justify-content-between">
                <a href="{{ url_for('blog.admin_posts') }}" class="btn btn-secondary">取消</a>
                <button type="submit" class="btn btn-primary">
                    {% if post %}更新文章{% else %}发布文章{% endif %}
                </button>
            </div>
        </form>
    </div>
</div>
{% endblock %}

{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js"></script>
<script>
    document.addEventListener('DOMContentLoaded', function() {
        // Initialize Markdown editor
        const easyMDE = new EasyMDE({
            element: document.getElementById('content'),
            spellChecker: false,
            autosave: {
                enabled: true,
                uniqueId: 'blog-post-content',
                delay: 1000,
            },
            toolbar: [
                'bold', 'italic', 'heading', '|', 
                'quote', 'unordered-list', 'ordered-list', '|',
                'link', 'image', 'code', 'table', '|',
                'preview', 'side-by-side', 'fullscreen', '|',
                'guide'
            ]
        });
        
        // Initialize Select2 for tags
        $('#tags').select2({
            tags: true,
            tokenSeparators: [','],
            placeholder: '输入标签,用逗号分隔'
        });
    });
</script>
{% endblock %}

app\templates\admin\tags.html

text/html 复制代码
{% extends "base.html" %}

{% block title %}标签管理 - Flask个人博客系统{% endblock %}

{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
    <h1>标签管理</h1>
    <a href="{{ url_for('blog.create_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

text/html 复制代码
{% extends "base.html" %}

{% block title %}
    {% if tag %}编辑标签{% else %}新建标签{% endif %} - Flask个人博客系统
{% endblock %}

{% block content %}
<div class="row">
    <div class="col-md-8">
        <div class="card">
            <div class="card-header">
                <h1 class="h3 mb-0">{% if tag %}编辑标签{% else %}新建标签{% endif %}</h1>
            </div>
            <div class="card-body">
                <form method="post" class="needs-validation" novalidate>
                    <div class="mb-3">
                        <label for="name" class="form-label">标签名称 <span class="text-danger">*</span></label>
                        <input type="text" class="form-control" id="name" name="name" 
                               value="{{ tag.name if tag else '' }}" required
                               minlength="2" maxlength="64" pattern="[^\s,]+"
                               placeholder="输入标签名称,不含空格或逗号">
                        <div class="invalid-feedback">
                            标签名称是必填项,且不能包含空格或逗号
                        </div>
                        <div class="form-text">
                            标签名称应简洁明了,避免使用空格或特殊字符
                        </div>
                    </div>
                    
                    {% if not tag %}
                    <div class="mb-3">
                        <div class="form-check">
                            <input class="form-check-input" type="checkbox" id="createMultiple" name="createMultiple">
                            <label class="form-check-label" for="createMultiple">
                                批量创建多个标签
                            </label>
                        </div>
                        <div id="multipleTagsSection" class="mt-2 d-none">
                            <label for="multipleNames" class="form-label">多个标签名称 <span class="text-danger">*</span></label>
                            <textarea class="form-control" id="multipleNames" name="multipleNames" rows="3"
                                      placeholder="输入多个标签,用逗号分隔"></textarea>
                            <div class="form-text">
                                输入多个标签名称,用逗号分隔,例如:技术,编程,Flask
                            </div>
                        </div>
                    </div>
                    {% endif %}
                    
                    {% if tag %}
                    <div class="mb-3">
                        <label class="form-label">使用情况</label>
                        <p class="form-control-static">
                            此标签已被用于 <strong>{{ tag.posts.count() }}</strong> 篇文章
                        </p>
                    </div>
                    
                    <div class="mb-3">
                        <label for="mergeTo" class="form-label">合并到其他标签</label>
                        <select class="form-select" id="mergeTo" name="mergeTo">
                            <option value="">-- 不合并 --</option>
                            {% for other_tag in tags %}
                                {% if other_tag.id != tag.id %}
                                <option value="{{ other_tag.id }}">{{ other_tag.name }}</option>
                                {% endif %}
                            {% endfor %}
                        </select>
                        <div class="form-text">
                            选择一个标签以将当前标签合并到该标签,所有关联的文章将被重新关联到新标签
                        </div>
                    </div>
                    {% endif %}
                    
                    <div class="d-flex justify-content-between">
                        <a href="{{ url_for('blog.admin_tags') }}" class="btn btn-secondary">
                            <i class="fas fa-arrow-left"></i> 返回标签列表
                        </a>
                        <button type="submit" class="btn btn-primary">
                            <i class="fas fa-save"></i> {% if tag %}更新标签{% else %}创建标签{% endif %}
                        </button>
                    </div>
                </form>
            </div>
        </div>
    </div>
    
    <div class="col-md-4">
        <div class="card">
            <div class="card-header">标签指南</div>
            <div class="card-body">
                <h5>创建有效的标签</h5>
                <ul>
                    <li>使用简洁明了的名称</li>
                    <li>避免使用空格,可以使用连字符或下划线</li>
                    <li>保持标签的一致性,例如使用单数形式</li>
                    <li>避免创建过于相似的标签</li>
                </ul>
                
                <h5>标签管理最佳实践</h5>
                <ul>
                    <li>定期检查和整理标签系统</li>
                    <li>合并相似的标签以保持系统整洁</li>
                    <li>删除很少使用的标签</li>
                    <li>为常用标签创建描述性名称</li>
                </ul>
            </div>
        </div>
        
        {% if tag and tag.posts.count() > 0 %}
        <div class="card mt-3">
            <div class="card-header">相关文章</div>
            <div class="card-body">
                <p>此标签下有 <strong>{{ tag.posts.count() }}</strong> 篇文章</p>
                <a href="{{ url_for('blog.tag', name=tag.name) }}" class="btn btn-outline-primary btn-sm" target="_blank">
                    <i class="fas fa-external-link-alt"></i> 查看标签页面
                </a>
            </div>
        </div>
        {% endif %}
        
        <div class="card mt-3">
            <div class="card-header">热门标签</div>
            <div class="card-body">
                <div class="popular-tags">
                    {% for popular_tag in popular_tags %}
                    <a href="{{ url_for('blog.tag', name=popular_tag.name) }}" class="badge bg-primary me-1 mb-1">
                        {{ popular_tag.name }} ({{ popular_tag.posts.count() }})
                    </a>
                    {% endfor %}
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block extra_js %}
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
<script>
    // 表单验证
    (function() {
        'use strict';
        
        // 获取所有需要验证的表单
        var forms = document.querySelectorAll('.needs-validation');
        
        // 循环并阻止提交
        Array.prototype.slice.call(forms).forEach(function(form) {
            form.addEventListener('submit', function(event) {
                if (!form.checkValidity()) {
                    event.preventDefault();
                    event.stopPropagation();
                }
                
                form.classList.add('was-validated');
            }, false);
        });
        
        // 批量创建标签切换
        const createMultipleCheckbox = document.getElementById('createMultiple');
        const multipleTagsSection = document.getElementById('multipleTagsSection');
        const nameInput = document.getElementById('name');
        const multipleNamesInput = document.getElementById('multipleNames');
        
        if (createMultipleCheckbox && multipleTagsSection) {
            createMultipleCheckbox.addEventListener('change', function() {
                if (this.checked) {
                    multipleTagsSection.classList.remove('d-none');
                    nameInput.required = false;
                    multipleNamesInput.required = true;
                } else {
                    multipleTagsSection.classList.add('d-none');
                    nameInput.required = true;
                    multipleNamesInput.required = false;
                }
            });
        }
        
        // 标签名称实时验证
        if (nameInput) {
            nameInput.addEventListener('input', function() {
                // 移除空格和逗号
                this.value = this.value.replace(/[\s,]/g, '');
            });
        }
    })();
</script>
{% endblock %}

app\templates\auth\change_password.html

text/html 复制代码
{% extends 'base.html' %}

{% block title %}修改密码 - Flask个人博客系统{% endblock %}

{% block content %}
<div class="container py-4">
    <div class="row">
        <div class="col-md-3">
            <div class="card mb-4">
                <div class="card-body text-center">
                    <div class="mb-3">
                        {% if current_user.avatar %}
                        <img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="img-fluid rounded-circle" style="width: 150px; height: 150px; object-fit: cover;">
                        {% else %}
                        <img src="/static/images/default-avatar.png" alt="{{ current_user.username }}" class="img-fluid rounded-circle" style="width: 150px; height: 150px; object-fit: cover;">
                        {% endif %}
                    </div>
                    <h5 class="card-title">{{ current_user.username }}</h5>
                    <p class="text-muted">{{ current_user.email }}</p>
                    <p class="text-muted">
                        <small>注册于: {{ current_user.created_time.strftime('%Y-%m-%d') }}</small>
                    </p>
                    <p class="text-muted">
                        <small>上次登录: {{ current_user.last_seen.strftime('%Y-%m-%d %H:%M') }}</small>
                    </p>
                </div>
            </div>
            
            <div class="list-group mb-4">
                <a href="{{ url_for('auth.profile') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-person-fill me-2"></i> 个人资料
                </a>
                <a href="{{ url_for('auth.my_posts') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-file-text-fill me-2"></i> 我的文章
                </a>
                <a href="{{ url_for('auth.change_password') }}" class="list-group-item list-group-item-action active">
                    <i class="bi bi-key-fill me-2"></i> 修改密码
                </a>
                {% if current_user.is_admin %}
                <a href="{{ url_for('auth.user_list') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-people-fill me-2"></i> 用户管理
                </a>
                <a href="{{ url_for('blog.admin') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-speedometer2 me-2"></i> 管理仪表盘
                </a>
                {% endif %}
            </div>
        </div>
        
        <div class="col-md-9">
            <div class="card">
                <div class="card-header">
                    <h5 class="mb-0">修改密码</h5>
                </div>
                <div class="card-body">
                    <form method="POST" action="{{ url_for('auth.change_password') }}" class="needs-validation" novalidate>
                        <div class="mb-3">
                            <label for="current_password" class="form-label">当前密码</label>
                            <input type="password" class="form-control" id="current_password" name="current_password" required>
                        </div>
                        <div class="mb-3">
                            <label for="new_password" class="form-label">新密码</label>
                            <input type="password" class="form-control" id="new_password" name="new_password" required minlength="6">
                            <div class="form-text">密码长度至少为6个字符</div>
                        </div>
                        <div class="mb-3">
                            <label for="confirm_password" class="form-label">确认新密码</label>
                            <input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
                        </div>
                        <button type="submit" class="btn btn-primary">更新密码</button>
                    </form>
                </div>
            </div>
            
            <div class="card mt-4">
                <div class="card-header">
                    <h5 class="mb-0">密码安全提示</h5>
                </div>
                <div class="card-body">
                    <ul class="mb-0">
                        <li>使用至少8个字符的密码</li>
                        <li>包含大小写字母、数字和特殊字符</li>
                        <li>避免使用容易猜到的信息,如生日、姓名等</li>
                        <li>定期更换密码以提高安全性</li>
                        <li>不要在多个网站使用相同的密码</li>
                    </ul>
                </div>
            </div>
        </div>
    </div>
</div>

{% block scripts %}
<script>
    // 表单验证
    (function() {
        'use strict';
        window.addEventListener('load', function() {
            var forms = document.getElementsByClassName('needs-validation');
            var validation = Array.prototype.filter.call(forms, function(form) {
                form.addEventListener('submit', function(event) {
                    if (form.checkValidity() === false) {
                        event.preventDefault();
                        event.stopPropagation();
                    }
                    
                    // 检查密码是否匹配
                    var newPassword = document.getElementById('new_password');
                    var confirmPassword = document.getElementById('confirm_password');
                    
                    if (newPassword.value !== confirmPassword.value) {
                        confirmPassword.setCustomValidity('两次输入的密码不匹配');
                        event.preventDefault();
                        event.stopPropagation();
                    } else {
                        confirmPassword.setCustomValidity('');
                    }
                    
                    form.classList.add('was-validated');
                }, false);
            });
        }, false);
    })();
</script>
{% endblock %}
{% endblock %}

app\templates\auth\login.html

text/html 复制代码
{% extends 'base.html' %}

{% block title %}登录{% endblock %}

{% block content %}
<div class="container py-5">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card">
                <div class="card-header">
                    <h4 class="mb-0">用户登录</h4>
                </div>
                <div class="card-body">
                    <form method="POST" action="{{ url_for('auth.login') }}">
                        <div class="mb-3">
                            <label for="username" class="form-label">用户名</label>
                            <input type="text" class="form-control" id="username" name="username" required>
                        </div>
                        <div class="mb-3">
                            <label for="password" class="form-label">密码</label>
                            <input type="password" class="form-control" id="password" name="password" required>
                        </div>
                        <div class="mb-3 form-check">
                            <input type="checkbox" class="form-check-input" id="remember" name="remember">
                            <label class="form-check-label" for="remember">记住我</label>
                        </div>
                        <div class="d-grid gap-2">
                            <button type="submit" class="btn btn-primary">登录</button>
                        </div>
                    </form>
                </div>
                <div class="card-footer text-center">
                    <p class="mb-0">还没有账号? <a href="{{ url_for('auth.register') }}">立即注册</a></p>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

app\templates\auth\my_posts.html

text/html 复制代码
{% extends 'base.html' %}

{% block title %}我的文章 - Flask个人博客系统{% endblock %}

{% block content %}
<div class="container py-4">
    <div class="row">
        <div class="col-md-3">
            <div class="card mb-4">
                <div class="card-body text-center">
                    <div class="mb-3">
                        {% if current_user.avatar %}
                        <img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="img-fluid rounded-circle" style="width: 150px; height: 150px; object-fit: cover;">
                        {% else %}
                        <img src="/static/images/default-avatar.png" alt="{{ current_user.username }}" class="img-fluid rounded-circle" style="width: 150px; height: 150px; object-fit: cover;">
                        {% endif %}
                    </div>
                    <h5 class="card-title">{{ current_user.username }}</h5>
                    <p class="text-muted">{{ current_user.email }}</p>
                    <p class="text-muted">
                        <small>注册于: {{ current_user.created_time.strftime('%Y-%m-%d') }}</small>
                    </p>
                    <p class="text-muted">
                        <small>上次登录: {{ current_user.last_seen.strftime('%Y-%m-%d %H:%M') }}</small>
                    </p>
                </div>
            </div>
            
            <div class="list-group mb-4">
                <a href="{{ url_for('auth.profile') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-person-fill me-2"></i> 个人资料
                </a>
                <a href="{{ url_for('auth.my_posts') }}" class="list-group-item list-group-item-action active">
                    <i class="bi bi-file-text-fill me-2"></i> 我的文章
                </a>
                <a href="{{ url_for('auth.change_password') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-key-fill me-2"></i> 修改密码
                </a>
                {% if current_user.is_admin %}
                <a href="{{ url_for('auth.user_list') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-people-fill me-2"></i> 用户管理
                </a>
                {% endif %}
            </div>
        </div>
        
        <div class="col-md-9">
            <div class="card">
                <div class="card-header d-flex justify-content-between align-items-center">
                    <h5 class="mb-0">我的文章</h5>
                    <a href="{{ url_for('blog.create_post') }}" class="btn btn-sm btn-primary">
                        <i class="bi bi-plus-lg"></i> 写新文章
                    </a>
                </div>
                <div class="card-body">
                    {% if posts.items %}
                        <div class="table-responsive">
                            <table class="table table-hover">
                                <thead>
                                    <tr>
                                        <th>标题</th>
                                        <th>分类</th>
                                        <th>发布时间</th>
                                        <th>浏览</th>
                                        <th>评论</th>
                                        <th>操作</th>
                                    </tr>
                                </thead>
                                <tbody>
                                    {% for post in posts.items %}
                                    <tr>
                                        <td>
                                            <a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="text-decoration-none">{{ post.title }}</a>
                                        </td>
                                        <td>
                                            <span class="badge bg-primary">{{ post.category.name }}</span>
                                        </td>
                                        <td>{{ post.created_time.strftime('%Y-%m-%d') }}</td>
                                        <td>{{ post.views }}</td>
                                        <td>{{ post.comments.count() }}</td>
                                        <td>
                                            <div class="btn-group btn-group-sm">
                                                <a href="{{ url_for('blog.edit_post', id=post.id) }}" class="btn btn-outline-primary">
                                                    <i class="bi bi-pencil"></i>
                                                </a>
                                                <button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteModal{{ post.id }}">
                                                    <i class="bi bi-trash"></i>
                                                </button>
                                            </div>
                                            
                                            <!-- 删除确认模态框 -->
                                            <div class="modal fade" id="deleteModal{{ post.id }}" tabindex="-1" aria-labelledby="deleteModalLabel{{ post.id }}" aria-hidden="true">
                                                <div class="modal-dialog">
                                                    <div class="modal-content">
                                                        <div class="modal-header">
                                                            <h5 class="modal-title" id="deleteModalLabel{{ post.id }}">确认删除</h5>
                                                            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                                                        </div>
                                                        <div class="modal-body">
                                                            确定要删除文章 "{{ post.title }}" 吗?此操作不可撤销。
                                                        </div>
                                                        <div class="modal-footer">
                                                            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
                                                            <a href="{{ url_for('blog.delete_post', id=post.id) }}" class="btn btn-danger">确认删除</a>
                                                        </div>
                                                    </div>
                                                </div>
                                            </div>
                                        </td>
                                    </tr>
                                    {% endfor %}
                                </tbody>
                            </table>
                        </div>
                        
                        <!-- 分页 -->
                        {% if posts.pages > 1 %}
                        <nav aria-label="Page navigation" class="mt-4">
                            <ul class="pagination justify-content-center">
                                {% if posts.has_prev %}
                                    <li class="page-item">
                                        <a class="page-link" href="{{ url_for('auth.my_posts', page=posts.prev_num) }}" aria-label="Previous">
                                            <span aria-hidden="true"><<</span>
                                        </a>
                                    </li>
                                {% else %}
                                    <li class="page-item disabled">
                                        <a class="page-link" href="#" aria-label="Previous">
                                            <span aria-hidden="true"><<</span>
                                        </a>
                                    </li>
                                {% endif %}
                                
                                {% for page_num in posts.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
                                    {% if page_num %}
                                        {% if page_num == posts.page %}
                                        <li class="page-item active"><a class="page-link" href="#">{{ page_num }}</a></li>
                                        {% else %}
                                        <li class="page-item"><a class="page-link" href="{{ url_for('auth.my_posts', page=page_num) }}">{{ page_num }}</a></li>
                                        {% endif %}
                                    {% else %}
                                    <li class="page-item disabled"><a class="page-link" href="#">...</a></li>
                                    {% endif %}
                                {% endfor %}
                                
                                {% if posts.has_next %}
                                    <li class="page-item">
                                        <a class="page-link" href="{{ url_for('auth.my_posts', page=posts.next_num) }}" aria-label="Next">
                                            <span aria-hidden="true">>></span>
                                        </a>
                                    </li>
                                {% else %}
                                    <li class="page-item disabled">
                                        <a class="page-link" href="#" aria-label="Next">
                                            <span aria-hidden="true">>></span>
                                        </a>
                                    </li>
                                {% endif %}
                            </ul>
                        </nav>
                        {% endif %}
                    {% else %}
                        <div class="alert alert-info">
                            <p class="mb-0">您还没有发布任何文章</p>
                            <div class="mt-3">
                                <a href="{{ url_for('blog.create_post') }}" class="btn btn-primary">
                                    <i class="bi bi-plus-lg"></i> 写第一篇文章
                                </a>
                            </div>
                        </div>
                    {% endif %}
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

app\templates\auth\profile.html

text/html 复制代码
{% extends 'base.html' %}

{% block title %}个人资料 - Flask个人博客系统{% endblock %}

{% block content %}
<div class="container py-4">
    <div class="row">
        <div class="col-md-3">
            <div class="card mb-4">
                <div class="card-body text-center">
                    <div class="mb-3">
                        {% if current_user.avatar %}
                        <img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="img-fluid rounded-circle" style="width: 150px; height: 150px; object-fit: cover;">
                        {% else %}
                        <img src="/static/images/default-avatar.png" alt="{{ current_user.username }}" class="img-fluid rounded-circle" style="width: 150px; height: 150px; object-fit: cover;">
                        {% endif %}
                    </div>
                    <h5 class="card-title">{{ current_user.username }}</h5>
                    <p class="text-muted">{{ current_user.email }}</p>
                    <p class="text-muted">
                        <small>注册于: {{ current_user.created_time.strftime('%Y-%m-%d') }}</small>
                    </p>
                    <p class="text-muted">
                        <small>上次登录: {{ current_user.last_seen.strftime('%Y-%m-%d %H:%M') }}</small>
                    </p>
                </div>
            </div>
            
            <div class="list-group mb-4">
                <a href="{{ url_for('auth.profile') }}" class="list-group-item list-group-item-action active">
                    <i class="bi bi-person-fill me-2"></i> 个人资料
                </a>
                <a href="{{ url_for('auth.my_posts') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-file-text-fill me-2"></i> 我的文章
                </a>
                <a href="{{ url_for('auth.change_password') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-key-fill me-2"></i> 修改密码
                </a>
                {% if current_user.is_admin %}
                <a href="{{ url_for('auth.user_list') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-people-fill me-2"></i> 用户管理
                </a>
                <a href="{{ url_for('blog.admin') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-speedometer2 me-2"></i> 管理仪表盘
                </a>
                {% endif %}
            </div>
        </div>
        
        <div class="col-md-9">
            <div class="card">
                <div class="card-header">
                    <h5 class="mb-0">编辑个人资料</h5>
                </div>
                <div class="card-body">
                    <form method="POST" action="{{ url_for('auth.profile') }}" enctype="multipart/form-data">
                        <div class="mb-3">
                            <label for="username" class="form-label">用户名</label>
                            <input type="text" class="form-control" id="username" name="username" value="{{ current_user.username }}" required>
                        </div>
                        <div class="mb-3">
                            <label for="email" class="form-label">电子邮箱</label>
                            <input type="email" class="form-control" id="email" name="email" value="{{ current_user.email }}" required>
                        </div>
                        <div class="mb-3">
                            <label for="avatar" class="form-label">头像</label>
                            <input type="file" class="form-control" id="avatar" name="avatar" accept="image/*">
                            <div class="form-text">支持JPG、PNG格式,建议上传正方形图片</div>
                        </div>
                        <div class="mb-3">
                            <label for="about_me" class="form-label">关于我</label>
                            <textarea class="form-control" id="about_me" name="about_me" rows="5">{{ current_user.about_me or '' }}</textarea>
                        </div>
                        <button type="submit" class="btn btn-primary">保存更改</button>
                    </form>
                </div>
            </div>
            
            {% if current_user.about_me %}
            <div class="card mt-4">
                <div class="card-header">
                    <h5 class="mb-0">关于我</h5>
                </div>
                <div class="card-body">
                    <p>{{ current_user.about_me }}</p>
                </div>
            </div>
            {% endif %}
        </div>
    </div>
</div>
{% endblock %}

app\templates\auth\register.html

text/html 复制代码
{% extends 'base.html' %}

{% block title %}注册{% endblock %}

{% block content %}
<div class="container py-5">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card">
                <div class="card-header">
                    <h4 class="mb-0">用户注册</h4>
                </div>
                <div class="card-body">
                    <form method="POST" action="{{ url_for('auth.register') }}" class="needs-validation" novalidate>
                        <div class="mb-3">
                            <label for="username" class="form-label">用户名 <span class="text-danger">*</span></label>
                            <input type="text" class="form-control" id="username" name="username" required>
                            <div class="form-text">用户名将显示在您发布的文章和评论中</div>
                        </div>
                        <div class="mb-3">
                            <label for="email" class="form-label">电子邮箱 <span class="text-danger">*</span></label>
                            <input type="email" class="form-control" id="email" name="email" required>
                            <div class="form-text">我们不会向任何人分享您的邮箱</div>
                        </div>
                        <div class="mb-3">
                            <label for="password" class="form-label">密码 <span class="text-danger">*</span></label>
                            <input type="password" class="form-control" id="password" name="password" required minlength="6">
                            <div class="form-text">密码长度至少为6个字符</div>
                        </div>
                        <div class="mb-3">
                            <label for="confirm_password" class="form-label">确认密码 <span class="text-danger">*</span></label>
                            <input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
                        </div>
                        <div class="d-grid gap-2">
                            <button type="submit" class="btn btn-primary">注册</button>
                        </div>
                    </form>
                </div>
                <div class="card-footer text-center">
                    <p class="mb-0">已有账号? <a href="{{ url_for('auth.login') }}">立即登录</a></p>
                </div>
            </div>
        </div>
    </div>
</div>

{% block scripts %}
<script>
    // 表单验证
    (function() {
        'use strict';
        window.addEventListener('load', function() {
            var forms = document.getElementsByClassName('needs-validation');
            var validation = Array.prototype.filter.call(forms, function(form) {
                form.addEventListener('submit', function(event) {
                    if (form.checkValidity() === false) {
                        event.preventDefault();
                        event.stopPropagation();
                    }
                    
                    // 检查密码是否匹配
                    var password = document.getElementById('password');
                    var confirmPassword = document.getElementById('confirm_password');
                    
                    if (password.value !== confirmPassword.value) {
                        confirmPassword.setCustomValidity('两次输入的密码不匹配');
                        event.preventDefault();
                        event.stopPropagation();
                    } else {
                        confirmPassword.setCustomValidity('');
                    }
                    
                    form.classList.add('was-validated');
                }, false);
            });
        }, false);
    })();
</script>
{% endblock %}
{% endblock %}

app\templates\auth\user_list.html

text/html 复制代码
{% extends 'base.html' %}

{% block title %}用户管理 - Flask个人博客系统{% endblock %}

{% block content %}
<div class="container py-4">
    <div class="row">
        <div class="col-md-3">
            <div class="card mb-4">
                <div class="card-body text-center">
                    <div class="mb-3">
                        {% if current_user.avatar %}
                        <img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="img-fluid rounded-circle" style="width: 150px; height: 150px; object-fit: cover;">
                        {% else %}
                        <img src="/static/images/default-avatar.png" alt="{{ current_user.username }}" class="img-fluid rounded-circle" style="width: 150px; height: 150px; object-fit: cover;">
                        {% endif %}
                    </div>
                    <h5 class="card-title">{{ current_user.username }}</h5>
                    <p class="text-muted">{{ current_user.email }}</p>
                    <p class="text-muted">
                        <small>注册于: {{ current_user.created_time.strftime('%Y-%m-%d') }}</small>
                    </p>
                    <p class="text-muted">
                        <small>上次登录: {{ current_user.last_seen.strftime('%Y-%m-%d %H:%M') }}</small>
                    </p>
                </div>
            </div>
            
            <div class="list-group mb-4">
                <a href="{{ url_for('auth.profile') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-person-fill me-2"></i> 个人资料
                </a>
                <a href="{{ url_for('auth.my_posts') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-file-text-fill me-2"></i> 我的文章
                </a>
                <a href="{{ url_for('auth.change_password') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-key-fill me-2"></i> 修改密码
                </a>
                {% if current_user.is_admin %}
                <a href="{{ url_for('auth.user_list') }}" class="list-group-item list-group-item-action active">
                    <i class="bi bi-people-fill me-2"></i> 用户管理
                </a>
                <a href="{{ url_for('blog.admin') }}" class="list-group-item list-group-item-action">
                    <i class="bi bi-speedometer2 me-2"></i> 管理仪表盘
                </a>
                {% endif %}
            </div>
        </div>
        
        <div class="col-md-9">
            <div class="card">
                <div class="card-header d-flex justify-content-between align-items-center">
                    <h5 class="mb-0">用户管理</h5>
                    <div class="input-group" style="width: 300px;">
                        <input type="text" id="userSearchInput" class="form-control" placeholder="搜索用户...">
                        <button class="btn btn-outline-secondary" type="button" id="clearSearch">
                            <i class="bi bi-x"></i>
                        </button>
                    </div>
                </div>
                <div class="card-body">
                    <div class="table-responsive">
                        <table class="table table-hover">
                            <thead>
                                <tr>
                                    <th>ID</th>
                                    <th>用户名</th>
                                    <th>邮箱</th>
                                    <th>注册时间</th>
                                    <th>角色</th>
                                    <th>操作</th>
                                </tr>
                            </thead>
                            <tbody id="userTableBody">
                                {% for user in users %}
                                <tr>
                                    <td>{{ user.id }}</td>
                                    <td>
                                        <div class="d-flex align-items-center">
                                            {% if user.avatar %}
                                            <img src="{{ user.avatar }}" alt="{{ user.username }}" class="rounded-circle me-2" style="width: 32px; height: 32px; object-fit: cover;">
                                            {% else %}
                                            <img src="/static/images/default-avatar.png" alt="{{ user.username }}" class="rounded-circle me-2" style="width: 32px; height: 32px; object-fit: cover;">
                                            {% endif %}
                                            {{ user.username }}
                                        </div>
                                    </td>
                                    <td>{{ user.email }}</td>
                                    <td>{{ user.created_time.strftime('%Y-%m-%d') }}</td>
                                    <td>
                                        {% if user.is_admin %}
                                        <span class="badge bg-primary">管理员</span>
                                        {% else %}
                                        <span class="badge bg-secondary">普通用户</span>
                                        {% endif %}
                                    </td>
                                    <td>
                                        {% if user.id != current_user.id %}
                                        <div class="btn-group">
                                            <form action="{{ url_for('auth.toggle_admin', user_id=user.id) }}" method="POST" class="d-inline">
                                                <button type="submit" class="btn btn-sm btn-outline-primary me-1" title="{{ '取消管理员' if user.is_admin else '设为管理员' }}">
                                                    <i class="bi {{ 'bi-person-dash' if user.is_admin else 'bi-person-plus' }}"></i>
                                                </button>
                                            </form>
                                            <button type="button" class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteUserModal{{ user.id }}" title="删除用户">
                                                <i class="bi bi-trash"></i>
                                            </button>
                                        </div>
                                        
                                        <!-- 删除用户确认模态框 -->
                                        <div class="modal fade" id="deleteUserModal{{ user.id }}" tabindex="-1" aria-labelledby="deleteUserModalLabel{{ user.id }}" aria-hidden="true">
                                            <div class="modal-dialog">
                                                <div class="modal-content">
                                                    <div class="modal-header">
                                                        <h5 class="modal-title" id="deleteUserModalLabel{{ user.id }}">确认删除</h5>
                                                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                                                    </div>
                                                    <div class="modal-body">
                                                        <p>您确定要删除用户 <strong>{{ user.username }}</strong> 吗?</p>
                                                        <p class="text-danger">此操作不可逆,用户的所有数据将被删除。</p>
                                                    </div>
                                                    <div class="modal-footer">
                                                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
                                                        <form action="{{ url_for('auth.delete_user', user_id=user.id) }}" method="POST">
                                                            <button type="submit" class="btn btn-danger">确认删除</button>
                                                        </form>
                                                    </div>
                                                </div>
                                            </div>
                                        </div>
                                        {% else %}
                                        <span class="text-muted">当前用户</span>
                                        {% endif %}
                                    </td>
                                </tr>
                                {% endfor %}
                            </tbody>
                        </table>
                    </div>
                    
                    {% if not users %}
                    <div class="text-center py-4">
                        <p class="text-muted">暂无用户数据</p>
                    </div>
                    {% endif %}
                </div>
            </div>
            
            <div class="card mt-4">
                <div class="card-header">
                    <h5 class="mb-0">用户统计</h5>
                </div>
                <div class="card-body">
                    <div class="row text-center">
                        <div class="col-md-4">
                            <div class="border rounded p-3 mb-3">
                                <h3>{{ users|length }}</h3>
                                <p class="text-muted mb-0">总用户数</p>
                            </div>
                        </div>
                        <div class="col-md-4">
                            <div class="border rounded p-3 mb-3">
                                <h3>{{ users|selectattr('is_admin', 'eq', true)|list|length }}</h3>
                                <p class="text-muted mb-0">管理员数</p>
                            </div>
                        </div>
                        <div class="col-md-4">
                            <div class="border rounded p-3 mb-3">
                                <h3>{{ users|selectattr('is_admin', 'ne', true)|list|length }}</h3>
                                <p class="text-muted mb-0">普通用户数</p>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

{% block scripts %}
<script>
    // 用户搜索功能
    document.addEventListener('DOMContentLoaded', function() {
        const searchInput = document.getElementById('userSearchInput');
        const clearButton = document.getElementById('clearSearch');
        const tableBody = document.getElementById('userTableBody');
        const rows = tableBody.querySelectorAll('tr');
        
        searchInput.addEventListener('input', function() {
            const searchTerm = this.value.toLowerCase();
            
            rows.forEach(row => {
                const username = row.querySelector('td:nth-child(2)').textContent.toLowerCase();
                const email = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
                
                if (username.includes(searchTerm) || email.includes(searchTerm)) {
                    row.style.display = '';
                } else {
                    row.style.display = 'none';
                }
            });
        });
        
        clearButton.addEventListener('click', function() {
            searchInput.value = '';
            rows.forEach(row => {
                row.style.display = '';
            });
        });
    });
</script>
{% endblock %}
{% endblock %}

app\templates\blog\archives.html

text/html 复制代码
{% extends "base.html" %}
{% from "blog/sidebar.html" import render_sidebar %}

{% block title %}文章归档 - Flask个人博客系统{% endblock %}

{% block content %}
<div class="container">
    <div class="row">
        <div class="col-md-8">
            <div class="mb-4">
                <h2>文章归档</h2>
                <p class="text-muted">共 {{ post_count }} 篇文章</p>
            </div>
            
            {% if archives %}
                {% for year, months in archives.items() %}
                    <div class="card mb-4">
                        <div class="card-header bg-primary text-white">
                            <h3 class="mb-0">{{ year }}年</h3>
                        </div>
                        <div class="card-body">
                            {% for month, posts in months.items() %}
                                <div class="mb-4">
                                    <h4 class="border-bottom pb-2">{{ month }}月 ({{ posts|length }}篇)</h4>
                                    <div class="list-group">
                                        {% for post in posts %}
                                            <a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="list-group-item list-group-item-action">
                                                <div class="d-flex w-100 justify-content-between">
                                                    <h5 class="mb-1">{{ post.title }}</h5>
                                                    <small class="text-muted">{{ post.created_time.strftime('%Y-%m-%d') }}</small>
                                                </div>
                                                <div class="d-flex justify-content-between align-items-center">
                                                    <div>
                                                        <span class="badge bg-primary me-1">{{ post.category.name }}</span>
                                                        {% for tag in post.tags %}
                                                            <span class="badge bg-secondary me-1">{{ tag.name }}</span>
                                                        {% endfor %}
                                                    </div>
                                                    <div class="text-muted small">
                                                        <i class="bi bi-eye me-1"></i>{{ post.views }}
                                                        <i class="bi bi-chat-dots ms-2 me-1"></i>{{ post.comments.count() }}
                                                    </div>
                                                </div>
                                            </a>
                                        {% endfor %}
                                    </div>
                                </div>
                            {% endfor %}
                        </div>
                    </div>
                {% endfor %}
            {% else %}
                <div class="alert alert-info">
                    <p class="mb-0">暂无文章</p>
                </div>
            {% endif %}
        </div>
        
        <!-- 使用侧边栏组件 -->
        {{ render_sidebar(categories, tags, recent_posts=recent_posts, post_count=post_count, comment_count=comment_count) }}
    </div>
</div>
{% endblock %}

app\templates\blog\category.html

text/html 复制代码
{% extends "base.html" %}
{% from "blog/sidebar.html" import render_sidebar %}

{% block title %}分类: {{ category.name }} - Flask个人博客系统{% endblock %}

{% block content %}
<div class="container">
    <div class="row">
        <div class="col-md-8">
            <div class="mb-4">
                <h2>分类: {{ category.name }}</h2>
                <p class="text-muted">共 {{ posts.total }} 篇文章</p>
            </div>
            
            {% if posts.items %}
                {% for post in posts.items %}
                    <div class="card mb-4">
                        <div class="card-body">
                            <h3 class="card-title">
                                <a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="text-decoration-none">
                                    {{ post.title }}
                                </a>
                            </h3>
                            <div class="card-subtitle mb-2 text-muted small">
                                <i class="bi bi-calendar me-1"></i>{{ post.created_time.strftime('%Y-%m-%d') }} | 
                                <i class="bi bi-folder me-1"></i><a href="{{ url_for('blog.category', name=post.category.name) }}" class="text-decoration-none">{{ post.category.name }}</a> | 
                                <i class="bi bi-person me-1"></i>{{ post.author.username }}
                            </div>
                            <p class="card-text">{{ post.content|striptags|truncate(200) }}</p>
                            <div class="mb-2">
                                {% for tag in post.tags %}
                                    <a href="{{ url_for('blog.tag', name=tag.name) }}" class="badge bg-secondary text-decoration-none">{{ tag.name }}</a>
                                {% endfor %}
                            </div>
                            <div class="d-flex justify-content-between align-items-center">
                                <a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="btn btn-sm btn-primary">
                                    阅读全文 <i class="bi bi-arrow-right"></i>
                                </a>
                                <div class="text-muted small">
                                    <i class="bi bi-eye me-1"></i>{{ post.views }}
                                    <i class="bi bi-chat-dots ms-2 me-1"></i>{{ post.comments.count() }}
                                </div>
                            </div>
                        </div>
                    </div>
                {% endfor %}
                
                <!-- 分页 -->
                {% if posts.pages > 1 %}
                <nav aria-label="Page navigation">
                    <ul class="pagination justify-content-center">
                        {% if posts.has_prev %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('blog.category', name=category.name, page=posts.prev_num) }}" aria-label="Previous">
                                    <span aria-hidden="true"><<</span>
                                </a>
                            </li>
                        {% else %}
                            <li class="page-item disabled">
                                <a class="page-link" href="#" aria-label="Previous">
                                    <span aria-hidden="true"><<</span>
                                </a>
                            </li>
                        {% endif %}
                        
                        {% for page_num in posts.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
                            {% if page_num %}
                                {% if page_num == posts.page %}
                                <li class="page-item active"><a class="page-link" href="#">{{ page_num }}</a></li>
                                {% else %}
                                <li class="page-item"><a class="page-link" href="{{ url_for('blog.category', name=category.name, page=page_num) }}">{{ page_num }}</a></li>
                                {% endif %}
                            {% else %}
                            <li class="page-item disabled"><a class="page-link" href="#">...</a></li>
                            {% endif %}
                        {% endfor %}
                        
                        {% if posts.has_next %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('blog.category', name=category.name, page=posts.next_num) }}" aria-label="Next">
                                    <span aria-hidden="true">>></span>
                                </a>
                            </li>
                        {% else %}
                            <li class="page-item disabled">
                                <a class="page-link" href="#" aria-label="Next">
                                    <span aria-hidden="true">>></span>
                                </a>
                            </li>
                        {% endif %}
                    </ul>
                </nav>
                {% endif %}
            {% else %}
                <div class="alert alert-info">
                    <p class="mb-0">该分类下暂无文章</p>
                </div>
            {% endif %}
        </div>
        
        <!-- 使用侧边栏组件 -->
        {{ render_sidebar(categories, tags, recent_posts=recent_posts, post_count=post_count, comment_count=comment_count) }}
    </div>
</div>
{% endblock %}

app\templates\blog\comments.html

text/html 复制代码
{% macro render_comments(post, comments) %}
<div class="comments-section mt-5" id="comments-section">
    <h4 class="mb-4">评论 ({{ comments|length }})</h4>
    
    {% if current_user.is_authenticated %}
    <div class="card mb-4">
        <div class="card-body">
            <form id="comment-form" data-post-id="{{ post.id }}">
                <div class="mb-3">
                    <textarea class="form-control" id="comment-content" rows="3" placeholder="写下您的评论..." required></textarea>
                </div>
                <div class="d-flex justify-content-end">
                    <button type="submit" class="btn btn-primary">发表评论</button>
                </div>
            </form>
        </div>
    </div>
    {% else %}
    <div class="alert alert-info mb-4">
        请 <a href="{{ url_for('auth.login') }}">登录</a> 后发表评论
    </div>
    {% endif %}
    
    <div id="comments-container">
        {% if comments %}
            {% for comment in comments if not comment.parent %}
                {{ render_comment(comment) }}
            {% endfor %}
        {% else %}
        <div class="text-center py-4" id="no-comments-message">
            <p class="text-muted">暂无评论,快来发表第一条评论吧!</p>
        </div>
        {% endif %}
    </div>
</div>
{% endmacro %}

{% macro render_comment(comment) %}
<div class="comment card mb-3" id="comment-{{ comment.id }}">
    <div class="card-body">
        <div class="d-flex">
            <div class="flex-shrink-0">
                {% if comment.author.avatar %}
                <img src="{{ comment.author.avatar }}" alt="{{ comment.author.username }}" class="rounded-circle" width="50" height="50" style="object-fit: cover;">
                {% else %}
                <img src="/static/images/default-avatar.png" alt="{{ comment.author.username }}" class="rounded-circle" width="50" height="50">
                {% endif %}
            </div>
            <div class="flex-grow-1 ms-3">
                <div class="d-flex justify-content-between align-items-center">
                    <h6 class="mb-0">{{ comment.author.username }}</h6>
                    <small class="text-muted">{{ comment.created_time.strftime('%Y-%m-%d %H:%M') }}</small>
                </div>
                <div class="comment-content mt-2">
                    <p>{{ comment.content }}</p>
                </div>
                <div class="comment-actions mt-2">
                    {% if current_user.is_authenticated %}
                    <button class="btn btn-sm btn-link p-0 reply-button" data-comment-id="{{ comment.id }}">回复</button>
                    {% if current_user.id == comment.author.id or current_user.is_admin %}
                    <button class="btn btn-sm btn-link p-0 ms-2 edit-button" data-comment-id="{{ comment.id }}">编辑</button>
                    <button class="btn btn-sm btn-link p-0 ms-2 text-danger delete-button" data-comment-id="{{ comment.id }}">删除</button>
                    {% endif %}
                    {% endif %}
                </div>
                
                <!-- 回复表单,默认隐藏 -->
                {% if current_user.is_authenticated %}
                <div class="reply-form mt-3" id="reply-form-{{ comment.id }}" style="display: none;">
                    <form class="reply-comment-form" data-parent-id="{{ comment.id }}" data-post-id="{{ comment.post.id }}">
                        <div class="mb-3">
                            <textarea class="form-control reply-content" rows="2" placeholder="回复 {{ comment.author.username }}..." required></textarea>
                        </div>
                        <div class="d-flex justify-content-end">
                            <button type="button" class="btn btn-sm btn-secondary me-2 cancel-reply">取消</button>
                            <button type="submit" class="btn btn-sm btn-primary">回复</button>
                        </div>
                    </form>
                </div>
                
                <!-- 编辑表单,默认隐藏 -->
                {% if current_user.id == comment.author.id or current_user.is_admin %}
                <div class="edit-form mt-3" id="edit-form-{{ comment.id }}" style="display: none;">
                    <form class="edit-comment-form" data-comment-id="{{ comment.id }}">
                        <div class="mb-3">
                            <textarea class="form-control edit-content" rows="2" required>{{ comment.content }}</textarea>
                        </div>
                        <div class="d-flex justify-content-end">
                            <button type="button" class="btn btn-sm btn-secondary me-2 cancel-edit">取消</button>
                            <button type="submit" class="btn btn-sm btn-primary">保存</button>
                        </div>
                    </form>
                </div>
                {% endif %}
                {% endif %}
                
                <!-- 嵌套回复 -->
                {% if comment.replies.count() > 0 %}
                <div class="nested-comments mt-3">
                    {% for reply in comment.replies %}
                    <div class="nested-comment card mb-2" id="comment-{{ reply.id }}">
                        <div class="card-body py-2">
                            <div class="d-flex">
                                <div class="flex-shrink-0">
                                    {% if reply.author.avatar %}
                                    <img src="{{ reply.author.avatar }}" alt="{{ reply.author.username }}" class="rounded-circle" width="35" height="35" style="object-fit: cover;">
                                    {% else %}
                                    <img src="/static/images/default-avatar.png" alt="{{ reply.author.username }}" class="rounded-circle" width="35" height="35">
                                    {% endif %}
                                </div>
                                <div class="flex-grow-1 ms-2">
                                    <div class="d-flex justify-content-between align-items-center">
                                        <h6 class="mb-0 fs-6">{{ reply.author.username }}</h6>
                                        <small class="text-muted">{{ reply.created_time.strftime('%Y-%m-%d %H:%M') }}</small>
                                    </div>
                                    <div class="comment-content mt-1">
                                        <p class="mb-1">{{ reply.content }}</p>
                                    </div>
                                    <div class="comment-actions">
                                        {% if current_user.is_authenticated %}
                                        <button class="btn btn-sm btn-link p-0 reply-button" data-comment-id="{{ comment.id }}">回复</button>
                                        {% if current_user.id == reply.author.id or current_user.is_admin %}
                                        <button class="btn btn-sm btn-link p-0 ms-2 edit-button" data-comment-id="{{ reply.id }}">编辑</button>
                                        <button class="btn btn-sm btn-link p-0 ms-2 text-danger delete-button" data-comment-id="{{ reply.id }}">删除</button>
                                        {% endif %}
                                        {% endif %}
                                    </div>
                                    
                                    <!-- 编辑表单,默认隐藏 -->
                                    {% if current_user.is_authenticated and (current_user.id == reply.author.id or current_user.is_admin) %}
                                    <div class="edit-form mt-2" id="edit-form-{{ reply.id }}" style="display: none;">
                                        <form class="edit-comment-form" data-comment-id="{{ reply.id }}">
                                            <div class="mb-2">
                                                <textarea class="form-control edit-content" rows="2" required>{{ reply.content }}</textarea>
                                            </div>
                                            <div class="d-flex justify-content-end">
                                                <button type="button" class="btn btn-sm btn-secondary me-2 cancel-edit">取消</button>
                                                <button type="submit" class="btn btn-sm btn-primary">保存</button>
                                            </div>
                                        </form>
                                    </div>
                                    {% endif %}
                                </div>
                            </div>
                        </div>
                    </div>
                    {% endfor %}
                </div>
                {% endif %}
            </div>
        </div>
    </div>
</div>
{% endmacro %}

app\templates\blog\create_post.html

text/html 复制代码
{% extends 'base.html' %}

{% block title %}创建文章 - Flask个人博客系统{% endblock %}

{% block styles %}
<!-- 引入Summernote编辑器 -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/summernote-bs4.min.css" rel="stylesheet">
<style>
    .note-editor .dropdown-toggle::after {
        display: none;
    }
</style>
{% endblock %}

{% block content %}
<div class="container py-4">
    <div class="row">
        <div class="col-md-12">
            <div class="card">
                <div class="card-header">
                    <h5 class="mb-0">创建新文章</h5>
                </div>
                <div class="card-body">
                    <form method="POST" action="{{ url_for('blog.create_post') }}" enctype="multipart/form-data">
                        <div class="mb-3">
                            <label for="title" class="form-label">标题</label>
                            <input type="text" class="form-control" id="title" name="title" required>
                        </div>
                        
                        <div class="row mb-3">
                            <div class="col-md-6">
                                <label for="category" class="form-label">分类</label>
                                <select class="form-select" id="category" name="category_id" required>
                                    <option value="" selected disabled>选择分类</option>
                                    {% for category in categories %}
                                    <option value="{{ category.id }}">{{ category.name }}</option>
                                    {% endfor %}
                                </select>
                            </div>
                            <div class="col-md-6">
                                <label for="tags" class="form-label">标签</label>
                                <select class="form-select" id="tags" name="tags" multiple data-placeholder="选择标签">
                                    {% for tag in tags %}
                                    <option value="{{ tag.id }}">{{ tag.name }}</option>
                                    {% endfor %}
                                </select>
                                <div class="form-text">按住Ctrl键可以选择多个标签</div>
                            </div>
                        </div>
                        
                        <div class="mb-3">
                            <label for="cover_image" class="form-label">封面图片</label>
                            <input type="file" class="form-control" id="cover_image" name="cover_image" accept="image/*">
                            <div class="form-text">可选,支持JPG、PNG格式</div>
                        </div>
                        
                        <div class="mb-3">
                            <label for="content" class="form-label">内容</label>
                            <textarea class="form-control" id="content" name="content" rows="15" required></textarea>
                        </div>
                        
                        <div class="mb-3">
                            <div class="form-check">
                                <input class="form-check-input" type="checkbox" id="published" name="published" checked>
                                <label class="form-check-label" for="published">
                                    立即发布
                                </label>
                            </div>
                        </div>
                        
                        <div class="d-flex justify-content-between">
                            <a href="{{ url_for('auth.my_posts') }}" class="btn btn-secondary">取消</a>
                            <div>
                                <button type="submit" name="save_draft" value="1" class="btn btn-outline-primary me-2">保存草稿</button>
                                <button type="submit" class="btn btn-primary">发布文章</button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<!-- 引入Summernote编辑器 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/summernote-bs4.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/lang/summernote-zh-CN.min.js"></script>
<script>
    $(document).ready(function() {
        $('#content').summernote({
            placeholder: '在这里输入文章内容...',
            height: 400,
            lang: 'zh-CN',
            toolbar: [
                ['style', ['style']],
                ['font', ['bold', 'underline', 'clear']],
                ['color', ['color']],
                ['para', ['ul', 'ol', 'paragraph']],
                ['table', ['table']],
                ['insert', ['link', 'picture', 'video']],
                ['view', ['fullscreen', 'codeview', 'help']]
            ],
            callbacks: {
                onImageUpload: function(files) {
                    // 这里可以实现图片上传功能
                    for (let i = 0; i < files.length; i++) {
                        uploadImage(files[i], this);
                    }
                }
            }
        });
        
        // 图片上传函数
        function uploadImage(file, editor) {
            const formData = new FormData();
            formData.append('file', file);
            
            $.ajax({
                url: '{{ url_for("blog.upload_image") }}',
                method: 'POST',
                data: formData,
                contentType: false,
                processData: false,
                success: function(data) {
                    if (data.success) {
                        $(editor).summernote('insertImage', data.url, function($image) {
                            $image.css('max-width', '100%');
                        });
                    } else {
                        alert('图片上传失败: ' + data.message);
                    }
                },
                error: function() {
                    alert('图片上传失败,请稍后重试');
                }
            });
        }
    });
</script>
{% endblock %}

app\templates\blog\edit_post.html

text/html 复制代码
{% extends 'base.html' %}

{% block title %}编辑文章 - Flask个人博客系统{% endblock %}

{% block styles %}
<!-- 引入Summernote编辑器 -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/summernote-bs4.min.css" rel="stylesheet">
<style>
    .note-editor .dropdown-toggle::after {
        display: none;
    }
</style>
{% endblock %}

{% block content %}
<div class="container py-4">
    <div class="row">
        <div class="col-md-12">
            <div class="card">
                <div class="card-header">
                    <h5 class="mb-0">编辑文章</h5>
                </div>
                <div class="card-body">
                    <form method="POST" action="{{ url_for('blog.edit_post', id=post.id) }}" enctype="multipart/form-data">
                        <div class="mb-3">
                            <label for="title" class="form-label">标题</label>
                            <input type="text" class="form-control" id="title" name="title" value="{{ post.title }}" required>
                        </div>
                        
                        <div class="row mb-3">
                            <div class="col-md-6">
                                <label for="category" class="form-label">分类</label>
                                <select class="form-select" id="category" name="category_id" required>
                                    <option value="" disabled>选择分类</option>
                                    {% for category in categories %}
                                    <option value="{{ category.id }}" {% if category.id == post.category_id %}selected{% endif %}>{{ category.name }}</option>
                                    {% endfor %}
                                </select>
                            </div>
                            <div class="col-md-6">
                                <label for="tags" class="form-label">标签</label>
                                <select class="form-select" id="tags" name="tags" multiple data-placeholder="选择标签">
                                    {% for tag in tags %}
                                    <option value="{{ tag.id }}" {% if tag in post.tags %}selected{% endif %}>{{ tag.name }}</option>
                                    {% endfor %}
                                </select>
                                <div class="form-text">按住Ctrl键可以选择多个标签</div>
                            </div>
                        </div>
                        
                        <div class="mb-3">
                            <label for="cover_image" class="form-label">封面图片</label>
                            {% if post.cover_image %}
                            <div class="mb-2">
                                <img src="{{ post.cover_image }}" alt="当前封面图" class="img-thumbnail" style="max-height: 200px;">
                                <div class="form-text">当前封面图片</div>
                            </div>
                            {% endif %}
                            <input type="file" class="form-control" id="cover_image" name="cover_image" accept="image/*">
                            <div class="form-text">可选,支持JPG、PNG格式。如果不上传新图片,将保留原有封面图</div>
                        </div>
                        
                        <div class="mb-3">
                            <label for="content" class="form-label">内容</label>
                            <textarea class="form-control" id="content" name="content" rows="15" required>{{ post.content }}</textarea>
                        </div>
                        
                        <div class="mb-3">
                            <div class="form-check">
                                <input class="form-check-input" type="checkbox" id="published" name="published" {% if post.published %}checked{% endif %}>
                                <label class="form-check-label" for="published">
                                    发布
                                </label>
                            </div>
                        </div>
                        
                        <div class="d-flex justify-content-between">
                            <a href="{{ url_for('auth.my_posts') }}" class="btn btn-secondary">取消</a>
                            <div>
                                <button type="submit" name="save_draft" value="1" class="btn btn-outline-primary me-2">保存草稿</button>
                                <button type="submit" class="btn btn-primary">更新文章</button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<!-- 引入Summernote编辑器 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/summernote-bs4.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/lang/summernote-zh-CN.min.js"></script>
<script>
    $(document).ready(function() {
        $('#content').summernote({
            placeholder: '在这里输入文章内容...',
            height: 400,
            lang: 'zh-CN',
            toolbar: [
                ['style', ['style']],
                ['font', ['bold', 'underline', 'clear']],
                ['color', ['color']],
                ['para', ['ul', 'ol', 'paragraph']],
                ['table', ['table']],
                ['insert', ['link', 'picture', 'video']],
                ['view', ['fullscreen', 'codeview', 'help']]
            ],
            callbacks: {
                onImageUpload: function(files) {
                    // 这里可以实现图片上传功能
                    for (let i = 0; i < files.length; i++) {
                        uploadImage(files[i], this);
                    }
                }
            }
        });
        
        // 图片上传函数
        function uploadImage(file, editor) {
            const formData = new FormData();
            formData.append('file', file);
            
            $.ajax({
                url: '{{ url_for("blog.upload_image") }}',
                method: 'POST',
                data: formData,
                contentType: false,
                processData: false,
                success: function(data) {
                    if (data.success) {
                        $(editor).summernote('insertImage', data.url, function($image) {
                            $image.css('max-width', '100%');
                        });
                    } else {
                        alert('图片上传失败: ' + data.message);
                    }
                },
                error: function() {
                    alert('图片上传失败,请稍后重试');
                }
            });
        }
    });
</script>
{% endblock %}

app\templates\blog\index.html

text/html 复制代码
{% extends "base.html" %}
{% from "blog/sidebar.html" import render_sidebar %}

{% block title %}首页 - Flask个人博客系统{% endblock %}

{% block content %}
<div class="container">
    <div class="row">
        <div class="col-md-8">
            <h2 class="mb-4">最新文章</h2>
            
            {% if posts.items %}
                {% for post in posts.items %}
                    <div class="card mb-4">
                        <div class="card-body">
                            <h3 class="card-title">
                                <a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="text-decoration-none">
                                    {{ post.title }}
                                </a>
                            </h3>
                            <div class="card-subtitle mb-2 text-muted small">
                                <i class="bi bi-calendar me-1"></i>{{ post.created_time.strftime('%Y-%m-%d') }} | 
                                <i class="bi bi-folder me-1"></i><a href="{{ url_for('blog.category', name=post.category.name) }}" class="text-decoration-none">{{ post.category.name }}</a> | 
                                <i class="bi bi-person me-1"></i>{{ post.author.username }}
                            </div>
                            <p class="card-text">{{ post.content|striptags|truncate(200) }}</p>
                            <div class="mb-2">
                                {% for tag in post.tags %}
                                    <a href="{{ url_for('blog.tag', name=tag.name) }}" class="badge bg-secondary text-decoration-none">{{ tag.name }}</a>
                                {% endfor %}
                            </div>
                            <div class="d-flex justify-content-between align-items-center">
                                <a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="btn btn-sm btn-primary">
                                    阅读全文 <i class="bi bi-arrow-right"></i>
                                </a>
                                <div class="text-muted small">
                                    <i class="bi bi-eye me-1"></i>{{ post.views }}
                                    <i class="bi bi-chat-dots ms-2 me-1"></i>{{ post.comments.count() }}
                                </div>
                            </div>
                        </div>
                    </div>
                {% endfor %}
                
                <!-- 分页 -->
                <nav aria-label="Page navigation">
                    <ul class="pagination justify-content-center">
                        {% if posts.has_prev %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('blog.index', page=posts.prev_num) }}" aria-label="Previous">
                                    <span aria-hidden="true"><<</span>
                                </a>
                            </li>
                        {% else %}
                            <li class="page-item disabled">
                                <a class="page-link" href="#" aria-label="Previous">
                                    <span aria-hidden="true"><<</span>
                                </a>
                            </li>
                        {% endif %}
                        
                        {% for page_num in posts.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
                            {% if page_num %}
                                {% if page_num == posts.page %}
                                <li class="page-item active"><a class="page-link" href="#">{{ page_num }}</a></li>
                                {% else %}
                                <li class="page-item"><a class="page-link" href="{{ url_for('blog.index', page=page_num) }}">{{ page_num }}</a></li>
                                {% endif %}
                            {% else %}
                            <li class="page-item disabled"><a class="page-link" href="#">...</a></li>
                            {% endif %}
                        {% endfor %}
                        
                        {% if posts.has_next %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('blog.index', page=posts.next_num) }}" aria-label="Next">
                                    <span aria-hidden="true">>></span>
                                </a>
                            </li>
                        {% else %}
                            <li class="page-item disabled">
                                <a class="page-link" href="#" aria-label="Next">
                                    <span aria-hidden="true">>></span>
                                </a>
                            </li>
                        {% endif %}
                    </ul>
                </nav>
            {% else %}
                <div class="alert alert-info">
                    <p class="mb-0">暂无文章</p>
                </div>
            {% endif %}
        </div>
        
        <!-- 使用侧边栏组件 -->
        {{ render_sidebar(categories, tags, recent_posts=recent_posts, post_count=post_count, comment_count=comment_count) }}
    </div>
</div>
{% endblock %}

app\templates\blog\post_detail.html

text/html 复制代码
{% extends "base.html" %}
{% from "blog/sidebar.html" import render_sidebar %}

{% block title %}{{ post.title }} - Flask个人博客系统{% endblock %}

{% block extra_css %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/themes/prism.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
<style>
    .blog-post-content img {
        max-width: 100%;
        height: auto;
    }
    .comment-avatar {
        width: 50px;
        height: 50px;
        border-radius: 50%;
    }
    .comment-form {
        margin-bottom: 2rem;
    }
    .comment-item {
        margin-bottom: 1.5rem;
    }
    .comment-reply {
        margin-left: 3rem;
    }
    .comment-actions {
        font-size: 0.8rem;
    }
</style>
{% endblock %}

{% block content %}
<div class="row">
    <div class="col-md-8">
        <article class="blog-post">
            <h1 class="mb-3">{{ post.title }}</h1>
            <div class="mb-3 text-muted">
                <small>
                    发布于: {{ post.created_time.strftime('%Y-%m-%d %H:%M') }} | 
                    分类: <a href="{{ url_for('blog.category', name=post.category.name) }}">{{ post.category.name }}</a> | 
                    作者: {{ post.author.username }}
                    {% if post.updated_time and post.updated_time != post.created_time %}
                    | 更新于: {{ post.updated_time.strftime('%Y-%m-%d %H:%M') }}
                    {% endif %}
                </small>
            </div>
            
            <div class="mb-3">
                {% for tag in post.tags %}
                    <a href="{{ url_for('blog.tag', name=tag.name) }}" class="badge bg-secondary text-decoration-none">{{ tag.name }}</a>
                {% endfor %}
            </div>
            
            <div class="blog-post-content">
                {{ post.content|safe }}
            </div>
        </article>
        
        <hr class="my-5">
        
        <!-- Comments Section -->
        {% from 'blog/comments.html' import render_comments %}
        {{ render_comments(post, comments) }}
    </div>
    
    <div class="col-md-4">
        {{ render_sidebar() }}
        
        <div class="card mb-4">
            <div class="card-header">相关文章</div>
            <div class="card-body">
                <ul class="list-unstyled">
                    {% for related_post in related_posts %}
                        <li class="mb-2">
                            <a href="{{ url_for('blog.post_detail', slug=related_post.slug) }}">{{ related_post.title }}</a>
                        </li>
                    {% else %}
                        <li>暂无相关文章</li>
                    {% endfor %}
                </ul>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/prism.min.js"></script>
<script src="{{ url_for('static', filename='js/comments.js') }}"></script>
{% endblock %}

app\templates\blog\search.html

text/html 复制代码
{% extends "base.html" %}
{% from "blog/sidebar.html" import render_sidebar %}

{% block title %}搜索结果: {{ query }} - Flask个人博客系统{% endblock %}

{% block content %}
<div class="container">
    <div class="row">
        <div class="col-md-8">
            <div class="mb-4">
                <h2>搜索结果: "{{ query }}"</h2>
                <p class="text-muted">共找到 {{ posts.total }} 篇相关文章</p>
            </div>
            
            <!-- 搜索表单 -->
            <div class="card mb-4">
                <div class="card-body">
                    <form action="{{ url_for('blog.search') }}" method="get" class="d-flex">
                        <input type="text" name="q" class="form-control me-2" placeholder="搜索文章..." value="{{ query }}" required>
                        <button type="submit" class="btn btn-primary">
                            <i class="bi bi-search"></i> 搜索
                        </button>
                    </form>
                </div>
            </div>
            
            {% if posts.items %}
                {% for post in posts.items %}
                    <div class="card mb-4">
                        <div class="card-body">
                            <h3 class="card-title">
                                <a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="text-decoration-none">
                                    {{ post.title|replace(query, '<mark>' + query + '</mark>')|safe if query in post.title else post.title }}
                                </a>
                            </h3>
                            <div class="card-subtitle mb-2 text-muted small">
                                <i class="bi bi-calendar me-1"></i>{{ post.created_time.strftime('%Y-%m-%d') }} | 
                                <i class="bi bi-folder me-1"></i><a href="{{ url_for('blog.category', name=post.category.name) }}" class="text-decoration-none">{{ post.category.name }}</a> | 
                                <i class="bi bi-person me-1"></i>{{ post.author.username }}
                            </div>
                            <p class="card-text">
                                {% set content_preview = post.content|striptags|truncate(200) %}
                                {{ content_preview|replace(query, '<mark>' + query + '</mark>')|safe if query in content_preview else content_preview }}
                            </p>
                            <div class="mb-2">
                                {% for tag in post.tags %}
                                    <a href="{{ url_for('blog.tag', name=tag.name) }}" class="badge bg-secondary text-decoration-none">{{ tag.name }}</a>
                                {% endfor %}
                            </div>
                            <div class="d-flex justify-content-between align-items-center">
                                <a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="btn btn-sm btn-primary">
                                    阅读全文 <i class="bi bi-arrow-right"></i>
                                </a>
                                <div class="text-muted small">
                                    <i class="bi bi-eye me-1"></i>{{ post.views }}
                                    <i class="bi bi-chat-dots ms-2 me-1"></i>{{ post.comments.count() }}
                                </div>
                            </div>
                        </div>
                    </div>
                {% endfor %}
                
                <!-- 分页 -->
                {% if posts.pages > 1 %}
                <nav aria-label="Page navigation">
                    <ul class="pagination justify-content-center">
                        {% if posts.has_prev %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('blog.search', q=query, page=posts.prev_num) }}" aria-label="Previous">
                                    <span aria-hidden="true"><<</span>
                                </a>
                            </li>
                        {% else %}
                            <li class="page-item disabled">
                                <a class="page-link" href="#" aria-label="Previous">
                                    <span aria-hidden="true"><<</span>
                                </a>
                            </li>
                        {% endif %}
                        
                        {% for page_num in posts.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
                            {% if page_num %}
                                {% if page_num == posts.page %}
                                <li class="page-item active"><a class="page-link" href="#">{{ page_num }}</a></li>
                                {% else %}
                                <li class="page-item"><a class="page-link" href="{{ url_for('blog.search', q=query, page=page_num) }}">{{ page_num }}</a></li>
                                {% endif %}
                            {% else %}
                            <li class="page-item disabled"><a class="page-link" href="#">...</a></li>
                            {% endif %}
                        {% endfor %}
                        
                        {% if posts.has_next %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('blog.search', q=query, page=posts.next_num) }}" aria-label="Next">
                                    <span aria-hidden="true">>></span>
                                </a>
                            </li>
                        {% else %}
                            <li class="page-item disabled">
                                <a class="page-link" href="#" aria-label="Next">
                                    <span aria-hidden="true">>></span>
                                </a>
                            </li>
                        {% endif %}
                    </ul>
                </nav>
                {% endif %}
            {% else %}
                <div class="alert alert-info">
                    <p class="mb-0">未找到与 "{{ query }}" 相关的文章</p>
                    <p class="mt-2">搜索建议:</p>
                    <ul>
                        <li>确保所有单词拼写正确</li>
                        <li>尝试使用更通用的关键词</li>
                        <li>尝试使用更少的关键词</li>
                    </ul>
                </div>
            {% endif %}
        </div>
        
        <!-- 使用侧边栏组件 -->
        {{ render_sidebar(categories, tags, recent_posts=recent_posts, post_count=post_count, comment_count=comment_count) }}
    </div>
</div>
{% endblock %}

app\templates\blog\sidebar.html

text/html 复制代码
{% macro render_sidebar(categories, tags, recent_posts=None) %}
<div class="col-md-4">
    <!-- 搜索框 -->
    <div class="card mb-4">
        <div class="card-header">
            <i class="bi bi-search me-2"></i>搜索
        </div>
        <div class="card-body">
            <form action="{{ url_for('blog.search') }}" method="get">
                <div class="input-group">
                    <input type="text" class="form-control" name="q" placeholder="搜索文章..." required>
                    <button class="btn btn-primary" type="submit">
                        <i class="bi bi-search"></i>
                    </button>
                </div>
            </form>
        </div>
    </div>
    
    <!-- 分类列表 -->
    <div class="card mb-4">
        <div class="card-header">
            <i class="bi bi-folder me-2"></i>分类
        </div>
        <div class="card-body">
            <ul class="list-group list-group-flush">
                {% for category in categories %}
                <li class="list-group-item d-flex justify-content-between align-items-center">
                    <a href="{{ url_for('blog.category', name=category.name) }}" class="text-decoration-none">
                        {{ category.name }}
                    </a>
                    <span class="badge bg-primary rounded-pill">{{ category.posts.count() }}</span>
                </li>
                {% else %}
                <li class="list-group-item">暂无分类</li>
                {% endfor %}
            </ul>
        </div>
    </div>
    
    <!-- 标签云 -->
    <div class="card mb-4">
        <div class="card-header">
            <i class="bi bi-tags me-2"></i>标签云
        </div>
        <div class="card-body">
            <div class="d-flex flex-wrap gap-2">
                {% for tag in tags %}
                <a href="{{ url_for('blog.tag', name=tag.name) }}" class="text-decoration-none">
                    <span class="badge bg-secondary fs-6">{{ tag.name }}</span>
                </a>
                {% else %}
                <span>暂无标签</span>
                {% endfor %}
            </div>
        </div>
    </div>
    
    <!-- 最近文章 -->
    {% if recent_posts %}
    <div class="card mb-4">
        <div class="card-header">
            <i class="bi bi-clock-history me-2"></i>最近文章
        </div>
        <div class="card-body">
            <ul class="list-group list-group-flush">
                {% for post in recent_posts %}
                <li class="list-group-item">
                    <a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="text-decoration-none">
                        {{ post.title }}
                    </a>
                    <div class="small text-muted mt-1">
                        <i class="bi bi-calendar me-1"></i>{{ post.created_time.strftime('%Y-%m-%d') }}
                    </div>
                </li>
                {% else %}
                <li class="list-group-item">暂无文章</li>
                {% endfor %}
            </ul>
        </div>
    </div>
    {% endif %}
    
    <!-- 博客统计 -->
    <div class="card mb-4">
        <div class="card-header">
            <i class="bi bi-bar-chart me-2"></i>统计信息
        </div>
        <div class="card-body">
            <ul class="list-group list-group-flush">
                <li class="list-group-item d-flex justify-content-between align-items-center">
                    文章总数
                    <span class="badge bg-primary rounded-pill">{{ post_count }}</span>
                </li>
                <li class="list-group-item d-flex justify-content-between align-items-center">
                    评论总数
                    <span class="badge bg-primary rounded-pill">{{ comment_count }}</span>
                </li>
                <li class="list-group-item d-flex justify-content-between align-items-center">
                    分类总数
                    <span class="badge bg-primary rounded-pill">{{ categories|length }}</span>
                </li>
                <li class="list-group-item d-flex justify-content-between align-items-center">
                    标签总数
                    <span class="badge bg-primary rounded-pill">{{ tags|length }}</span>
                </li>
            </ul>
        </div>
    </div>
</div>
{% endmacro %}

app\templates\blog\tag.html

text/html 复制代码
{% extends "base.html" %}
{% from "blog/sidebar.html" import render_sidebar %}

{% block title %}标签: {{ tag.name }} - Flask个人博客系统{% endblock %}

{% block content %}
<div class="container">
    <div class="row">
        <div class="col-md-8">
            <div class="mb-4">
                <h2>标签: {{ tag.name }}</h2>
                <p class="text-muted">共 {{ posts.total }} 篇文章</p>
            </div>
            
            {% if posts.items %}
                {% for post in posts.items %}
                    <div class="card mb-4">
                        <div class="card-body">
                            <h3 class="card-title">
                                <a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="text-decoration-none">
                                    {{ post.title }}
                                </a>
                            </h3>
                            <div class="card-subtitle mb-2 text-muted small">
                                <i class="bi bi-calendar me-1"></i>{{ post.created_time.strftime('%Y-%m-%d') }} | 
                                <i class="bi bi-folder me-1"></i><a href="{{ url_for('blog.category', name=post.category.name) }}" class="text-decoration-none">{{ post.category.name }}</a> | 
                                <i class="bi bi-person me-1"></i>{{ post.author.username }}
                            </div>
                            <p class="card-text">{{ post.content|striptags|truncate(200) }}</p>
                            <div class="mb-2">
                                {% for post_tag in post.tags %}
                                    <a href="{{ url_for('blog.tag', name=post_tag.name) }}" class="badge bg-secondary text-decoration-none">{{ post_tag.name }}</a>
                                {% endfor %}
                            </div>
                            <div class="d-flex justify-content-between align-items-center">
                                <a href="{{ url_for('blog.post_detail', slug=post.slug) }}" class="btn btn-sm btn-primary">
                                    阅读全文 <i class="bi bi-arrow-right"></i>
                                </a>
                                <div class="text-muted small">
                                    <i class="bi bi-eye me-1"></i>{{ post.views }}
                                    <i class="bi bi-chat-dots ms-2 me-1"></i>{{ post.comments.count() }}
                                </div>
                            </div>
                        </div>
                    </div>
                {% endfor %}
                
                <!-- 分页 -->
                {% if posts.pages > 1 %}
                <nav aria-label="Page navigation">
                    <ul class="pagination justify-content-center">
                        {% if posts.has_prev %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('blog.tag', name=tag.name, page=posts.prev_num) }}" aria-label="Previous">
                                    <span aria-hidden="true"><<</span>
                                </a>
                            </li>
                        {% else %}
                            <li class="page-item disabled">
                                <a class="page-link" href="#" aria-label="Previous">
                                    <span aria-hidden="true"><<</span>
                                </a>
                            </li>
                        {% endif %}
                        
                        {% for page_num in posts.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
                            {% if page_num %}
                                {% if page_num == posts.page %}
                                <li class="page-item active"><a class="page-link" href="#">{{ page_num }}</a></li>
                                {% else %}
                                <li class="page-item"><a class="page-link" href="{{ url_for('blog.tag', name=tag.name, page=page_num) }}">{{ page_num }}</a></li>
                                {% endif %}
                            {% else %}
                            <li class="page-item disabled"><a class="page-link" href="#">...</a></li>
                            {% endif %}
                        {% endfor %}
                        
                        {% if posts.has_next %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('blog.tag', name=tag.name, page=posts.next_num) }}" aria-label="Next">
                                    <span aria-hidden="true">>></span>
                                </a>
                            </li>
                        {% else %}
                            <li class="page-item disabled">
                                <a class="page-link" href="#" aria-label="Next">
                                    <span aria-hidden="true">>></span>
                                </a>
                            </li>
                        {% endif %}
                    </ul>
                </nav>
                {% endif %}
            {% else %}
                <div class="alert alert-info">
                    <p class="mb-0">该标签下暂无文章</p>
                </div>
            {% endif %}
        </div>
        
        <!-- 使用侧边栏组件 -->
        {{ render_sidebar(categories, tags, recent_posts=recent_posts, post_count=post_count, comment_count=comment_count) }}
    </div>
</div>
{% endblock %}

app\templates\errors\404.html

text/html 复制代码
{% extends 'base.html' %}

{% block title %}页面未找到{% endblock %}

{% block content %}
<div class="container py-5">
    <div class="row justify-content-center">
        <div class="col-md-8 text-center">
            <h1 class="display-1 fw-bold">404</h1>
            <p class="fs-3">页面未找到</p>
            <p class="lead">您访问的页面不存在或已被移除。</p>
            <div class="mt-4">
                <a href="{{ url_for('blog.index') }}" class="btn btn-primary">返回首页</a>
            </div>
        </div>
    </div>
</div>
{% endblock %}

app\templates\errors\500.html

text/html 复制代码
{% extends 'base.html' %}

{% block title %}服务器错误{% endblock %}

{% block content %}
<div class="container py-5">
    <div class="row justify-content-center">
        <div class="col-md-8 text-center">
            <h1 class="display-1 fw-bold">500</h1>
            <p class="fs-3">服务器内部错误</p>
            <p class="lead">抱歉,服务器遇到了一个错误。我们正在努力修复这个问题。</p>
            <div class="mt-4">
                <a href="{{ url_for('blog.index') }}" class="btn btn-primary">返回首页</a>
            </div>
        </div>
    </div>
</div>
{% endblock %}

blog.sql

复制代码
-- 博客系统数据库结构

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- 用户表
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(80) NOT NULL,
  `email` varchar(120) NOT NULL,
  `password_hash` varchar(128) NOT NULL,
  `avatar` varchar(255) DEFAULT NULL,
  `bio` text,
  `website` varchar(255) DEFAULT NULL,
  `location` varchar(100) DEFAULT NULL,
  `is_admin` tinyint(1) DEFAULT '0',
  `created_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `last_login` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`),
  UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- ----------------------------
-- 分类表
-- ----------------------------
DROP TABLE IF EXISTS `category`;
CREATE TABLE `category` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `description` varchar(255) DEFAULT NULL,
  `order` int(11) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章分类表';

-- ----------------------------
-- 标签表
-- ----------------------------
DROP TABLE IF EXISTS `tag`;
CREATE TABLE `tag` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章标签表';

-- ----------------------------
-- 文章表
-- ----------------------------
DROP TABLE IF EXISTS `post`;
CREATE TABLE `post` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(128) NOT NULL,
  `slug` varchar(128) NOT NULL,
  `content` text NOT NULL,
  `summary` text,
  `cover_image` varchar(255) DEFAULT NULL,
  `views` int(11) DEFAULT '0',
  `likes` int(11) DEFAULT '0',
  `published` tinyint(1) DEFAULT '1',
  `created_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `updated_time` datetime DEFAULT NULL,
  `author_id` int(11) NOT NULL,
  `category_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `slug` (`slug`),
  KEY `author_id` (`author_id`),
  KEY `category_id` (`category_id`),
  KEY `created_time` (`created_time`),
  CONSTRAINT `post_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
  CONSTRAINT `post_ibfk_2` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章表';

-- ----------------------------
-- 文章标签关联表
-- ----------------------------
DROP TABLE IF EXISTS `post_tag`;
CREATE TABLE `post_tag` (
  `post_id` int(11) NOT NULL,
  `tag_id` int(11) NOT NULL,
  PRIMARY KEY (`post_id`,`tag_id`),
  KEY `tag_id` (`tag_id`),
  CONSTRAINT `post_tag_ibfk_1` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE CASCADE,
  CONSTRAINT `post_tag_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tag` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章标签关联表';

-- ----------------------------
-- 评论表
-- ----------------------------
DROP TABLE IF EXISTS `comment`;
CREATE TABLE `comment` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content` text NOT NULL,
  `created_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `post_id` int(11) NOT NULL,
  `author_id` int(11) NOT NULL,
  `parent_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `post_id` (`post_id`),
  KEY `author_id` (`author_id`),
  KEY `parent_id` (`parent_id`),
  CONSTRAINT `comment_ibfk_1` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE CASCADE,
  CONSTRAINT `comment_ibfk_2` FOREIGN KEY (`author_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
  CONSTRAINT `comment_ibfk_3` FOREIGN KEY (`parent_id`) REFERENCES `comment` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表';

-- ----------------------------
-- 系统设置表
-- ----------------------------
DROP TABLE IF EXISTS `setting`;
CREATE TABLE `setting` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `key` varchar(50) NOT NULL,
  `value` text,
  `description` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `key` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统设置表';

-- ----------------------------
-- 页面表
-- ----------------------------
DROP TABLE IF EXISTS `page`;
CREATE TABLE `page` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(128) NOT NULL,
  `slug` varchar(128) NOT NULL,
  `content` text NOT NULL,
  `published` tinyint(1) DEFAULT '1',
  `created_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `updated_time` datetime DEFAULT NULL,
  `author_id` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `slug` (`slug`),
  KEY `author_id` (`author_id`),
  CONSTRAINT `page_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='页面表';

-- ----------------------------
-- 导航菜单表
-- ----------------------------
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `url` varchar(255) NOT NULL,
  `order` int(11) DEFAULT '0',
  `parent_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `parent_id` (`parent_id`),
  CONSTRAINT `menu_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `menu` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='导航菜单表';

-- ----------------------------
-- 文件上传表
-- ----------------------------
DROP TABLE IF EXISTS `upload`;
CREATE TABLE `upload` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `filename` varchar(255) NOT NULL,
  `original_filename` varchar(255) NOT NULL,
  `mime_type` varchar(100) NOT NULL,
  `size` int(11) NOT NULL,
  `path` varchar(255) NOT NULL,
  `created_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `user_id` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`),
  CONSTRAINT `upload_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件上传表';

-- ----------------------------
-- 初始数据
-- ----------------------------

-- 管理员用户 (密码: admin123)
INSERT INTO `user` (`username`, `email`, `password_hash`, `is_admin`, `created_time`) VALUES
('admin', '[email protected]', 'pbkdf2:sha256:150000$nMmI0Hcg$d89f36192a427a34d5c9b4c64daa46b5d87011b761e44f7c28b70e35bc8df6bb', 1, NOW());

-- 测试用户 (密码: password123)
INSERT INTO `user` (`username`, `email`, `password_hash`, `bio`, `created_time`) VALUES
('test', '[email protected]', 'pbkdf2:sha256:150000$GkVCcgbA$a2d78f9b02de3c9c3d2c593b77d50d2a1a7388eaf3693a1347a834eedb1c83e9', '这是一个测试用户', NOW());

-- 分类数据
INSERT INTO `category` (`name`, `description`, `order`) VALUES
('技术', '技术相关文章', 1),
('生活', '生活随笔', 2),
('教程', '各类教程', 3),
('资源', '资源分享', 4),
('其他', '其他分类', 5);

-- 标签数据
INSERT INTO `tag` (`name`) VALUES
('Python'),
('JavaScript'),
('Flask'),
('Web开发'),
('数据库'),
('前端'),
('后端'),
('AI'),
('机器学习'),
('Linux');

-- 系统设置
INSERT INTO `setting` (`key`, `value`, `description`) VALUES
('site_name', '我的博客', '网站名称'),
('site_description', '一个使用Flask开发的博客系统', '网站描述'),
('site_keywords', '博客,Flask,Python,Web开发', '网站关键词'),
('posts_per_page', '10', '每页显示文章数'),
('comments_enabled', '1', '是否开启评论'),
('registration_enabled', '1', '是否开启注册'),
('admin_email', '[email protected]', '管理员邮箱');

-- 导航菜单
INSERT INTO `menu` (`name`, `url`, `order`, `parent_id`) VALUES
('首页', '/', 1, NULL),
('归档', '/archives', 2, NULL),
('分类', '/category/技术', 3, NULL),
('关于', '/page/about', 4, NULL);

-- 示例文章
INSERT INTO `post` (`title`, `slug`, `content`, `summary`, `published`, `created_time`, `author_id`, `category_id`) VALUES
('欢迎使用Flask博客系统', 'welcome-to-flask-blog', '# 欢迎使用Flask博客系统\n\n这是一个使用Flask开发的博客系统,具有以下特点:\n\n- 文章管理\n- 分类和标签\n- 评论系统\n- 用户管理\n- 响应式设计\n\n## 如何使用\n\n1. 注册账号\n2. 登录系统\n3. 开始发布您的文章\n\n祝您使用愉快!', '这是一个使用Flask开发的博客系统,具有文章管理、分类和标签、评论系统、用户管理、响应式设计等特点。', 1, NOW(), 1, 1),
('Flask入门教程', 'flask-tutorial', '# Flask入门教程\n\n## 什么是Flask\n\nFlask是一个轻量级的Python Web框架,易于学习和使用,同时又足够灵活,可以满足各种需求。\n\n## 安装Flask\n\n```python\npip install flask\n```\n\n## 创建第一个Flask应用\n\n```python\nfrom flask import Flask\n\napp = Flask(__name__)\n\[email protected](\"/\")\ndef hello_world():\n    return \"<p>Hello, World!</p>\"\n\nif __name__ == \"__main__\":\n    app.run(debug=True)\n```\n\n## 路由和视图函数\n\nFlask使用装饰器来定义路由...\n\n## 模板渲染\n\nFlask使用Jinja2作为模板引擎...\n\n## 表单处理\n\n使用Flask-WTF可以方便地处理表单...\n\n## 数据库集成\n\nFlask-SQLAlchemy提供了ORM支持...\n\n## 结语\n\n通过本教程,您已经了解了Flask的基础知识,可以开始构建自己的Web应用了。', 'Flask是一个轻量级的Python Web框架,本教程介绍了Flask的基础知识,包括安装、创建应用、路由、模板、表单和数据库等内容。', 1, NOW(), 1, 3);

-- 文章标签关联
INSERT INTO `post_tag` (`post_id`, `tag_id`) VALUES
(1, 3), -- 欢迎使用Flask博客系统 - Flask
(1, 4), -- 欢迎使用Flask博客系统 - Web开发
(2, 1), -- Flask入门教程 - Python
(2, 3), -- Flask入门教程 - Flask
(2, 4), -- Flask入门教程 - Web开发
(2, 7); -- Flask入门教程 - 后端

-- 示例页面
INSERT INTO `page` (`title`, `slug`, `content`, `published`, `created_time`, `author_id`) VALUES
('关于我们', 'about', '# 关于我们\n\n这是一个使用Flask开发的博客系统,旨在提供一个简洁、高效的博客平台。\n\n## 联系方式\n\n- 邮箱:[email protected]\n- GitHub:https://github.com/yourusername/flask-blog\n\n欢迎提出宝贵意见和建议!', 1, NOW(), 1);

-- 示例评论
INSERT INTO `comment` (`content`, `created_time`, `post_id`, `author_id`) VALUES
('这是一个很棒的博客系统,期待更多功能!', NOW(), 1, 2),
('教程写得非常清晰,对初学者很友好!', NOW(), 2, 2);

SET FOREIGN_KEY_CHECKS = 1;
相关推荐
橙色小博10 分钟前
长短期记忆神经网络(LSTM)基础学习与实例:预测序列的未来
人工智能·python·深度学习·神经网络·lstm
不知名。。。。。。。。12 分钟前
C++__list
开发语言·c++·list
SsummerC13 分钟前
【leetcode100】每日温度
数据结构·python·leetcode
仙人掌_lz24 分钟前
机器学习ML极简指南
人工智能·python·算法·机器学习·面试·强化学习
船长@Quant26 分钟前
PyTorch量化进阶教程:第六章 模型部署与生产化
pytorch·python·深度学习·transformer·量化交易·sklearn·ta-lib
叫我王富贵i40 分钟前
0基础入门scrapy 框架,获取豆瓣top250存入mysql
爬虫·python·scrapy
EverestVIP41 分钟前
C++动态库对外接口通过接口方式实现
开发语言·c++
Swift社区1 小时前
Swift LeetCode 246 题解:中心对称数(Strobogrammatic Number)
开发语言·leetcode·swift
巷北夜未央1 小时前
Python每日一题(13)
开发语言·python·算法
woniu_maggie1 小时前
SAP EXCEL DOI 详解
开发语言·后端·excel