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

《AI大模型趣味实战》No2 : 快速搭建一个漂亮的AI家庭网站-相册/时间线/日历/多用户/个性化配色(中)

快速搭建一个漂亮的AI家庭网站-相册/时间线/日历/多用户/个性化配色(中)

摘要

在上一篇文章中,我们介绍了如何搭建一个基础的家庭网站(V1.0版本),包含了用户管理、相册管理、时间线和日历等功能。本文将继续深入,详细介绍V1.1和V1.2版本中新增的功能,主要包括博客系统的实现、富文本编辑器的集成、评论和点赞功能,以及日程管理系统的优化。通过这些功能的添加,我们的家庭网站将更加完善,为家庭成员提供更丰富的交流和记录方式。

代码仓 https://github.com/wyg5208/family_website_V1_2
在这里插入图片描述

一、V1.1版本新增功能

1. 博客系统

V1.1版本最重要的更新是添加了完整的博客系统,让家庭成员可以发表文章,分享生活点滴和心得体会。

1.1 数据模型设计

首先,我们需要设计博客相关的数据模型,包括文章、评论和点赞:

# app/models.py

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    content = db.Column(db.Text, nullable=False)
    summary = db.Column(db.String(200))
    cover_image = db.Column(db.String(120))
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    
    # 关系
    user = db.relationship('User', backref=db.backref('posts', lazy=True))
    comments = db.relationship('Comment', backref='post', lazy=True, cascade='all, delete-orphan')
    likes = db.relationship('Like', backref='post', lazy=True, cascade='all, delete-orphan')
    
    def __repr__(self):
        return f'<Post {self.title}>'
    
    @property
    def comment_count(self):
        return len(self.comments)
    
    @property
    def like_count(self):
        return len(self.likes)

class Comment(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    content = db.Column(db.Text, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
    parent_id = db.Column(db.Integer, db.ForeignKey('comment.id'))
    
    # 关系
    user = db.relationship('User', backref=db.backref('comments', lazy=True))
    replies = db.relationship('Comment', backref=db.backref('parent', remote_side=[id]), lazy=True)
    
    def __repr__(self):
        return f'<Comment {self.id}>'

class Like(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
    
    # 关系
    user = db.relationship('User', backref=db.backref('likes', lazy=True))
    
    __table_args__ = (db.UniqueConstraint('user_id', 'post_id', name='unique_user_post_like'),)
    
    def __repr__(self):
        return f'<Like {self.id}>'
1.2 博客首页实现

博客首页展示最新文章列表,包括文章标题、摘要、作者和发布时间等信息:

<!-- app/templates/blog/index.html -->
{% extends 'base.html' %}

{% block title %}家庭博客 - 家庭网站{% endblock %}

{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
    <h1><i class="fas fa-blog me-2"></i>家庭博客</h1>
    {% if current_user.is_authenticated %}
    <a href="{{ url_for('blog.create') }}" class="btn btn-primary">
        <i class="fas fa-pen-to-square me-1"></i>写文章
    </a>
    {% endif %}
</div>

{% if posts %}
<div class="row row-cols-1 row-cols-md-2 g-4">
    {% for post in posts %}
    <div class="col">
        <div class="card h-100 blog-card">
            {% if post.cover_image %}
            <img src="{{ url_for('static', filename='uploads/blog/' + post.cover_image) }}" class="card-img-top" alt="{{ post.title }}">
            {% endif %}
            <div class="card-body">
                <h5 class="card-title">{{ post.title }}</h5>
                <p class="card-text text-muted small">
                    <i class="far fa-user me-1"></i>{{ post.user.username }} | 
                    <i class="far fa-calendar me-1"></i>{{ post.created_at.strftime('%Y-%m-%d') }}
                </p>
                <p class="card-text">{{ post.summary or (post.content|striptags|truncate(150)) }}</p>
            </div>
            <div class="card-footer d-flex justify-content-between align-items-center">
                <div>
                    <span class="badge bg-primary me-1"><i class="far fa-comment me-1"></i>{{ post.comment_count }}</span>
                    <span class="badge bg-danger"><i class="far fa-heart me-1"></i>{{ post.like_count }}</span>
                </div>
                <a href="{{ url_for('blog.view', post_id=post.id) }}" class="btn btn-sm btn-outline-primary">阅读全文</a>
            </div>
        </div>
    </div>
    {% endfor %}
</div>

<!-- 分页控件 -->
<nav aria-label="Page navigation" class="mt-4">
    <ul class="pagination justify-content-center">
        {% if pagination.has_prev %}
        <li class="page-item">
            <a class="page-link" href="{{ url_for('blog.index', page=pagination.prev_num) }}" aria-label="Previous">
                <span aria-hidden="true">&laquo;</span>
            </a>
        </li>
        {% else %}
        <li class="page-item disabled">
            <a class="page-link" href="#" aria-label="Previous">
                <span aria-hidden="true">&laquo;</span>
            </a>
        </li>
        {% endif %}
        
        {% for page in pagination.iter_pages() %}
            {% if page %}
                {% if page != pagination.page %}
                <li class="page-item"><a class="page-link" href="{{ url_for('blog.index', page=page) }}">{{ page }}</a></li>
                {% else %}
                <li class="page-item active"><a class="page-link" href="#">{{ page }}</a></li>
                {% endif %}
            {% else %}
                <li class="page-item disabled"><a class="page-link" href="#">...</a></li>
            {% endif %}
        {% endfor %}
        
        {% if pagination.has_next %}
        <li class="page-item">
            <a class="page-link" href="{{ url_for('blog.index', page=pagination.next_num) }}" aria-label="Next">
                <span aria-hidden="true">&raquo;</span>
            </a>
        </li>
        {% else %}
        <li class="page-item disabled">
            <a class="page-link" href="#" aria-label="Next">
                <span aria-hidden="true">&raquo;</span>
            </a>
        </li>
        {% endif %}
    </ul>
</nav>
{% else %}
<div class="alert alert-info">
    <i class="fas fa-info-circle me-2"></i>目前还没有博客文章。
    {% if current_user.is_authenticated %}
    <a href="{{ url_for('blog.create') }}" class="alert-link">点击这里</a>创建第一篇文章!
    {% endif %}
</div>
{% endif %}
{% endblock %}
1.3 集成CKEditor 5富文本编辑器

为了提供更好的写作体验,我们集成了CKEditor 5富文本编辑器:

<!-- app/templates/blog/edit.html -->
{% extends 'base.html' %}

{% block title %}{{ '编辑文章' if post else '创建文章' }} - 家庭网站{% endblock %}

{% block styles %}
<style>
    .ck-editor__editable {
        min-height: 300px;
    }
</style>
{% endblock %}

{% block content %}
<nav aria-label="breadcrumb" class="mb-4">
    <ol class="breadcrumb">
        <li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">首页</a></li>
        <li class="breadcrumb-item"><a href="{{ url_for('blog.index') }}">博客</a></li>
        <li class="breadcrumb-item active" aria-current="page">{{ '编辑文章' if post else '创建文章' }}</li>
    </ol>
</nav>

<div class="card">
    <div class="card-header">
        <h2>{{ '编辑文章' if post else '创建文章' }}</h2>
    </div>
    <div class="card-body">
        <form id="postForm" method="POST" enctype="multipart/form-data">
            {{ form.hidden_tag() }}
            
            <div class="mb-3">
                {{ form.title.label(class="form-label") }}
                {{ form.title(class="form-control" + (" is-invalid" if form.title.errors else "")) }}
                {% for error in form.title.errors %}
                <div class="invalid-feedback">{{ error }}</div>
                {% endfor %}
            </div>
            
            <div class="mb-3">
                {{ form.summary.label(class="form-label") }}
                {{ form.summary(class="form-control" + (" is-invalid" if form.summary.errors else "")) }}
                {% for error in form.summary.errors %}
                <div class="invalid-feedback">{{ error }}</div>
                {% endfor %}
                <div class="form-text">简短摘要,不超过200字符。如不填写,将自动从内容中提取。</div>
            </div>
            
            <div class="mb-3">
                {{ form.cover_image.label(class="form-label") }}
                {{ form.cover_image(class="form-control" + (" is-invalid" if form.cover_image.errors else "")) }}
                {% for error in form.cover_image.errors %}
                <div class="invalid-feedback">{{ error }}</div>
                {% endfor %}
                {% if post and post.cover_image %}
                <div class="mt-2">
                    <img src="{{ url_for('static', filename='uploads/blog/' + post.cover_image) }}" alt="当前封面图" class="img-thumbnail" style="max-height: 100px;">
                    <div class="form-text">当前封面图。上传新图片将替换此图。</div>
                </div>
                {% endif %}
            </div>
            
            <div class="mb-3">
                {{ form.content.label(class="form-label") }}
                {{ form.content(class="form-control" + (" is-invalid" if form.content.errors else ""), id="editor") }}
                {% for error in form.content.errors %}
                <div class="invalid-feedback">{{ error }}</div>
                {% endfor %}
            </div>
            
            <div class="d-flex justify-content-between">
                <a href="{{ url_for('blog.view', post_id=post.id) if post else url_for('blog.index') }}" class="btn btn-secondary">取消</a>
                <button type="submit" class="btn btn-primary">保存文章</button>
            </div>
        </form>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="https://cdn.ckeditor.com/ckeditor5/35.1.0/classic/ckeditor.js"></script>
<script>
    document.addEventListener('DOMContentLoaded', function() {
        let editor;
        
        ClassicEditor
            .create(document.querySelector('#editor'))
            .then(newEditor => {
                editor = newEditor;
            })
            .catch(error => {
                console.error(error);
            });
        
        const form = document.getElementById('postForm');
        const contentTextarea = document.querySelector('#editor');
        
        form.addEventListener('submit', function(e) {
            e.preventDefault();
            
            // 获取CKEditor内容
            const editorData = editor.getData();
            
            // 检查内容是否为空
            if (!editorData.trim()) {
                alert('文章内容不能为空!');
                return;
            }
            
            // 将CKEditor内容设置到原始textarea
            contentTextarea.value = editorData;
            
            // 提交表单
            this.submit();
        });
    });
</script>
{% endblock %}
1.4 评论和回复功能

博客系统支持评论和嵌套回复功能,让家庭成员可以互动交流:

<!-- 评论部分 (app/templates/blog/view.html的一部分) -->
<div class="card mt-4">
    <div class="card-header">
        <h3><i class="far fa-comments me-2"></i>评论 ({{ post.comment_count }})</h3>
    </div>
    <div class="card-body">
        {% if current_user.is_authenticated %}
        <form method="POST" action="{{ url_for('blog.add_comment', post_id=post.id) }}">
            {{ comment_form.hidden_tag() }}
            <div class="mb-3">
                {{ comment_form.content.label(class="form-label") }}
                {{ comment_form.content(class="form-control" + (" is-invalid" if comment_form.content.errors else ""), rows=3) }}
                {% for error in comment_form.content.errors %}
                <div class="invalid-feedback">{{ error }}</div>
                {% endfor %}
            </div>
            <button type="submit" class="btn btn-primary">发表评论</button>
        </form>
        {% else %}
        <div class="alert alert-info"><a href="{{ url_for('auth.login', next=request.path) }}">登录</a>后发表评论。
        </div>
        {% endif %}
        
        <hr>
        
        <!-- 评论列表 -->
        {% if post.comments %}
        <div class="comments-container">
            {% for comment in top_level_comments %}
            <div class="comment mb-3">
                <div class="d-flex">
                    <div class="flex-shrink-0">
                        <img src="{{ url_for('static', filename='img/default-avatar.png') }}" class="rounded-circle" width="50" height="50" alt="{{ comment.user.username }}">
                    </div>
                    <div class="flex-grow-1 ms-3">
                        <div class="d-flex justify-content-between">
                            <h5 class="mt-0">{{ comment.user.username }}</h5>
                            <small class="text-muted">{{ comment.created_at.strftime('%Y-%m-%d %H:%M') }}</small>
                        </div>
                        <p>{{ comment.content }}</p>
                        
                        {% if current_user.is_authenticated %}
                        <button class="btn btn-sm btn-outline-secondary reply-btn" data-comment-id="{{ comment.id }}">
                            <i class="fas fa-reply me-1"></i>回复
                        </button>
                        
                        <div class="reply-form mt-2 d-none" id="reply-form-{{ comment.id }}">
                            <form method="POST" action="{{ url_for('blog.add_reply', post_id=post.id, comment_id=comment.id) }}">
                                {{ reply_form.hidden_tag() }}
                                <div class="mb-2">
                                    {{ reply_form.content(class="form-control", rows=2, placeholder="回复 " + comment.user.username) }}
                                </div>
                                <div class="d-flex justify-content-end">
                                    <button type="button" class="btn btn-sm btn-secondary me-2 cancel-reply-btn">取消</button>
                                    <button type="submit" class="btn btn-sm btn-primary">提交回复</button>
                                </div>
                            </form>
                        </div>
                        {% endif %}
                        
                        <!-- 嵌套回复 -->
                        {% if comment.replies %}
                        <div class="replies mt-3">
                            {% for reply in comment.replies %}
                            <div class="reply d-flex mt-2">
                                <div class="flex-shrink-0">
                                    <img src="{{ url_for('static', filename='img/default-avatar.png') }}" class="rounded-circle" width="40" height="40" alt="{{ reply.user.username }}">
                                </div>
                                <div class="flex-grow-1 ms-2">
                                    <div class="d-flex justify-content-between">
                                        <h6 class="mt-0">{{ reply.user.username }}</h6>
                                        <small class="text-muted">{{ reply.created_at.strftime('%Y-%m-%d %H:%M') }}</small>
                                    </div>
                                    <p>{{ reply.content }}</p>
                                </div>
                            </div>
                            {% endfor %}
                        </div>
                        {% endif %}
                    </div>
                </div>
            </div>
            {% endfor %}
        </div>
        {% else %}
        <div class="alert alert-light">
            暂无评论,快来发表第一条评论吧!
        </div>
        {% endif %}
    </div>
</div>
1.5 点赞功能

为了增加互动性,我们添加了文章点赞功能:

# app/blog/routes.py

@bp.route('/like/<int:post_id>', methods=['POST'])
@login_required
def like_post(post_id):
    post = Post.query.get_or_404(post_id)
    
    # 检查用户是否已经点赞
    existing_like = Like.query.filter_by(user_id=current_user.id, post_id=post_id).first()
    
    if existing_like:
        # 如果已经点赞,则取消点赞
        db.session.delete(existing_like)
        flash('已取消点赞', 'info')
    else:
        # 如果未点赞,则添加点赞
        like = Like(user_id=current_user.id, post_id=post_id)
        db.session.add(like)
        flash('点赞成功', 'success')
    
    db.session.commit()
    return redirect(url_for('blog.view', post_id=post_id))
<!-- 点赞按钮 (app/templates/blog/view.html的一部分) -->
<div class="d-flex justify-content-between align-items-center mt-4">
    <div>
        <span class="badge bg-primary me-2"><i class="far fa-comment me-1"></i>{{ post.comment_count }} 评论</span>
        <span class="badge bg-danger"><i class="far fa-heart me-1"></i>{{ post.like_count }} 点赞</span>
    </div>
    
    {% if current_user.is_authenticated %}
    <form method="POST" action="{{ url_for('blog.like_post', post_id=post.id) }}">
        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
        <button type="submit" class="btn btn-sm {% if has_liked %}btn-danger{% else %}btn-outline-danger{% endif %}">
            <i class="{% if has_liked %}fas{% else %}far{% endif %} fa-heart me-1"></i>
            {% if has_liked %}已点赞{% else %}点赞{% endif %}
        </button>
    </form>
    {% endif %}
</div>
1.6 用户博客页面

我们还添加了用户博客页面,展示特定用户发表的所有文章:

<!-- app/templates/blog/user_posts.html -->
{% extends 'base.html' %}

{% block title %}{{ user.username }}的博客 - 家庭网站{% endblock %}

{% block styles %}
<style>
    .user-info {
        background-color: var(--bs-tertiary-bg);
        border-radius: 10px;
        padding: 20px;
        margin-bottom: 30px;
    }
    
    .user-avatar {
        width: 100px;
        height: 100px;
        border-radius: 50%;
        object-fit: cover;
        border: 3px solid var(--bs-primary);
    }
    
    .user-stats {
        display: flex;
        gap: 20px;
        margin-top: 15px;
    }
    
    .stat-item {
        text-align: center;
    }
    
    .stat-value {
        font-size: 1.5rem;
        font-weight: bold;
        color: var(--bs-primary);
    }
    
    .stat-label {
        font-size: 0.9rem;
        color: var(--bs-secondary-color);
    }
    
    .blog-card {
        transition: transform 0.3s ease, box-shadow 0.3s ease;
    }
    
    .blog-card:hover {
        transform: translateY(-5px);
        box-shadow: 0 10px 20px rgba(0,0,0,0.1);
    }
    
    .blog-card .card-img-top {
        height: 180px;
        object-fit: cover;
    }
</style>
{% endblock %}

{% block content %}
<nav aria-label="breadcrumb" class="mb-4">
    <ol class="breadcrumb">
        <li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">首页</a></li>
        <li class="breadcrumb-item"><a href="{{ url_for('blog.index') }}">博客</a></li>
        <li class="breadcrumb-item active" aria-current="page">{{ user.username }}的博客</li>
    </ol>
</nav>

<!-- 用户信息 -->
<div class="user-info">
    <div class="d-flex align-items-center">
        <img src="{{ url_for('static', filename='img/default-avatar.png') }}" alt="{{ user.username }}" class="user-avatar me-4">
        <div>
            <h2>{{ user.username }}</h2>
            <p class="text-muted">{{ user.email }}</p>
            
            <div class="user-stats">
                <div class="stat-item">
                    <div class="stat-value">{{ user.posts|length }}</div>
                    <div class="stat-label">文章</div>
                </div>
                <div class="stat-item">
                    <div class="stat-value">{{ total_comments }}</div>
                    <div class="stat-label">收到的评论</div>
                </div>
                <div class="stat-item">
                    <div class="stat-value">{{ total_likes }}</div>
                    <div class="stat-label">收到的点赞</div>
                </div>
            </div>
        </div>
    </div>
</div>

<h3 class="mb-4">{{ user.username }}的文章</h3>

{% if current_user.is_authenticated and current_user.id == user.id %}
<div class="mb-4">
    <a href="{{ url_for('blog.create') }}" class="btn btn-primary">
        <i class="fas fa-pen-to-square me-1"></i>写新文章
    </a>
</div>
{% endif %}

{% if posts %}
<div class="row row-cols-1 row-cols-md-2 g-4">
    {% for post in posts %}
    <div class="col">
        <div class="card h-100 blog-card">
            {% if post.cover_image %}
            <img src="{{ url_for('static', filename='uploads/blog/' + post.cover_image) }}" class="card-img-top" alt="{{ post.title }}">
            {% endif %}
            <div class="card-body">
                <h5 class="card-title">{{ post.title }}</h5>
                <p class="card-text text-muted small">
                    <i class="far fa-calendar me-1"></i>{{ post.created_at.strftime('%Y-%m-%d') }}
                </p>
                <p class="card-text">{{ post.summary or (post.content|striptags|truncate(150)) }}</p>
            </div>
            <div class="card-footer d-flex justify-content-between align-items-center">
                <div>
                    <span class="badge bg-primary me-1"><i class="far fa-comment me-1"></i>{{ post.comment_count }}</span>
                    <span class="badge bg-danger"><i class="far fa-heart me-1"></i>{{ post.like_count }}</span>
                </div>
                <a href="{{ url_for('blog.view', post_id=post.id) }}" class="btn btn-sm btn-outline-primary">阅读全文</a>
            </div>
        </div>
    </div>
    {% endfor %}
</div>

<!-- 分页控件 -->
<nav aria-label="Page navigation" class="mt-4">
    <ul class="pagination justify-content-center">
        {% if pagination.has_prev %}
        <li class="page-item">
            <a class="page-link" href="{{ url_for('blog.user_posts', username=user.username, page=pagination.prev_num) }}" aria-label="Previous">
                <span aria-hidden="true">&laquo;</span>
            </a>
        </li>
        {% else %}
        <li class="page-item disabled">
            <a class="page-link" href="#" aria-label="Previous">
                <span aria-hidden="true">&laquo;</span>
            </a>
        </li>
        {% endif %}
        
        {% for page in pagination.iter_pages() %}
            {% if page %}
                {% if page != pagination.page %}
                <li class="page-item"><a class="page-link" href="{{ url_for('blog.user_posts', username=user.username, page=page) }}">{{ page }}</a></li>
                {% else %}
                <li class="page-item active"><a class="page-link" href="#">{{ page }}</a></li>
                {% endif %}
            {% else %}
                <li class="page-item disabled"><a class="page-link" href="#">...</a></li>
            {% endif %}
        {% endfor %}
        
        {% if pagination.has_next %}
        <li class="page-item">
            <a class="page-link" href="{{ url_for('blog.user_posts', username=user.username, page=pagination.next_num) }}" aria-label="Next">
                <span aria-hidden="true">&raquo;</span>
            </a>
        </li>
        {% else %}
        <li class="page-item disabled">
            <a class="page-link" href="#" aria-label="Next">
                <span aria-hidden="true">&raquo;</span>
            </a>
        </li>
        {% endif %}
    </ul>
</nav>
{% else %}
<div class="alert alert-info">
    <i class="fas fa-info-circle me-2"></i>{{ user.username }}还没有发表任何文章。
</div>
{% endif %}
{% endblock %}

二、V1.2版本新增功能

1. 日程管理系统优化

V1.2版本主要对日程管理系统进行了优化,解决了数据模型不一致的问题。

1.1 统一日程模型

在之前的版本中,我们有两个不同的模型(ScheduleCalendarEvent)来表示日程事件,这导致了数据不一致的问题。在V1.2版本中,我们统一使用Schedule模型:

# app/main/routes.py

@bp.route('/')
def index():
    # 获取轮播图
    carousel_items = CarouselItem.query.order_by(CarouselItem.order).all()
    
    # 获取即将到来的事件
    # 如果用户已登录,只显示当前用户的日程
    if current_user.is_authenticated:
        upcoming_events = Schedule.query.filter(
            Schedule.user_id == current_user.id,
            Schedule.start_time >= datetime.now()
        ).order_by(Schedule.start_time).limit(3).all()
    else:
        upcoming_events = []
    
    # 获取最近的时间线事件
    recent_events = TimelineEvent.query.order_by(TimelineEvent.date.desc()).limit(3).all()
    
    # 获取最新博客文章
    latest_posts = Post.query.order_by(Post.created_at.desc()).limit(3).all()
    
    return render_template('main/index.html', 
                          carousel_items=carousel_items,
                          upcoming_events=upcoming_events,
                          recent_events=recent_events,
                          latest_posts=latest_posts)
1.2 改进日历视图

在V1.2版本中,我们移除了日历视图的月份限制,现在可以显示所有日程:

# app/calendar/routes.py

@bp.route('/')
@login_required
def index():
    # 获取当前年月
    year = request.args.get('year', datetime.now().year, type=int)
    month = request.args.get('month', datetime.now().month, type=int)
    
    # 计算当前月的第一天和最后一天
    first_day = datetime(year, month, 1)
    
    # 获取下一个月的第一天
    if month == 12:
        next_month_first_day = datetime(year + 1, 1, 1)
    else:
        next_month_first_day = datetime(year, month + 1, 1)
    
    # 获取当前用户的所有日程,不再限制月份
    schedules = Schedule.query.filter_by(user_id=current_user.id).order_by(Schedule.start_time).all()
    
    # 计算上个月和下个月的链接
    prev_month = month - 1 if month > 1 else 12
    prev_year = year if month > 1 else year - 1
    
    next_month = month + 1 if month < 12 else 1
    next_year = year if month < 12 else year + 1
    
    # 生成日历数据
    cal = calendar.monthcalendar(year, month)
    
    return render_template('calendar/index.html',
                          year=year,
                          month=month,
                          calendar=cal,
                          schedules=schedules,
                          prev_month=prev_month,
                          prev_year=prev_year,
                          next_month=next_month,
                          next_year=next_year,
                          current_date=datetime.now().date())
1.3 扩展"即将到来的日程"页面时间范围

我们将"即将到来的日程"页面的时间范围从7天扩展到30天,以显示更多的日程:

# app/calendar/routes.py

@bp.route('/upcoming')
@login_required
def upcoming():
    # 获取当前日期和30天后的日期
    today = datetime.now().date()
    next_month = today + timedelta(days=30)
    
    # 获取未来30天内的日程
    schedules = Schedule.query.filter(
        Schedule.user_id == current_user.id,
        Schedule.start_time >= datetime.combine(today, time.min),
        Schedule.start_time <= datetime.combine(next_month, time.max)
    ).order_by(Schedule.start_time).all()
    
    return render_template('calendar/upcoming.html', schedules=schedules)
1.4 修复jQuery加载问题

在V1.2版本中,我们修复了jQuery加载问题,确保页面正常加载:

<!-- app/templates/base.html -->
<!DOCTYPE html>
<html lang="zh-CN" data-bs-theme="dark" data-bs-theme-color="blue">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}家庭网站{% endblock %}</title>
    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <!-- Font Awesome -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <!-- 自定义CSS -->
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    {% block styles %}{% endblock %}
</head>
<body>
    <!-- 导航栏 -->
    {% include 'includes/navbar.html' %}
    
    <!-- 主内容区 -->
    <main class="container mt-4">
        <!-- 闪现消息 -->
        {% include 'includes/flash_messages.html' %}
        
        <!-- 页面内容 -->
        {% block content %}{% endblock %}
    </main>
    
    <!-- 页脚 -->
    {% include 'includes/footer.html' %}
    
    <!-- Bootstrap JS -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
    <!-- jQuery -->
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <!-- 主题切换JS -->
    <script src="{{ url_for('static', filename='js/theme.js') }}"></script>
    <!-- 色彩主题切换JS -->
    <script src="{{ url_for('static', filename='js/color-theme.js') }}"></script>
    <!-- 自定义JS -->
    <script src="{{ url_for('static', filename='js/main.js') }}"></script>
    {% block scripts %}{% endblock %}
</body>
</html>
1.5 改进日程删除功能

我们使用现代JavaScript事件处理方式改进了日程删除功能:

<!-- app/templates/calendar/upcoming.html -->
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
    const deleteButtons = document.querySelectorAll('.delete-btn');
    deleteButtons.forEach(button => {
        button.addEventListener('click', function(e) {
            e.preventDefault();
            if (confirm('确定要删除这个日程吗?此操作不可撤销。')) {
                window.location.href = this.getAttribute('data-delete-url');
            }
        });
    });
});
</script>
{% endblock %}
<!-- 删除按钮部分 -->
<a href="#" class="btn btn-sm btn-outline-danger delete-btn" 
   data-delete-url="{{ url_for('calendar.delete_schedule', schedule_id=schedule.id) }}">
    <i class="fas fa-trash-alt"></i>
</a>

2. 首页"即将到来的事件"显示优化

我们优化了首页"即将到来的事件"的显示,使其更加美观和信息丰富:

<!-- app/templates/main/index.html (部分) -->
<!-- 即将到来的事件 -->
<div class="col-md-4">
    <div class="card h-100">
        <div class="card-header bg-primary text-white">
            <h5 class="card-title mb-0"><i class="fas fa-calendar-alt me-2"></i>即将到来的事件</h5>
        </div>
        <div class="card-body">
            {% if upcoming_events %}
            <div class="list-group list-group-flush">
                {% for event in upcoming_events %}
                <div class="list-group-item px-0">
                    <div class="d-flex w-100 justify-content-between">
                        <h5 class="mb-1">{{ event.title }}</h5>
                        <small class="text-muted">
                            {% if event.all_day %}
                            <span class="badge bg-info">全天</span>
                            {% else %}
                            {{ event.start_time.strftime('%H:%M') }}
                            {% endif %}
                        </small>
                    </div>
                    <p class="mb-1 text-muted">
                        <i class="far fa-calendar me-1"></i>{{ event.start_time.strftime('%Y-%m-%d') }}
                    </p>
                    {% if event.description %}
                    <p class="mb-1">{{ event.description|truncate(100) }}</p>
                    {% endif %}
                    <a href="{{ url_for('calendar.view_schedule', schedule_id=event.id) }}" class="btn btn-sm btn-outline-primary mt-2">
                        <i class="fas fa-eye me-1"></i>查看详情
                    </a>
                </div>
                {% endfor %}
            </div>
            {% else %}
            <div class="alert alert-info">
                <i class="fas fa-info-circle me-2"></i>未来没有即将到来的事件。
                {% if current_user.is_authenticated %}
                <a href="{{ url_for('calendar.create_schedule') }}" class="alert-link">点击这里</a>添加新日程!
                {% endif %}
            </div>
            {% endif %}
        </div>
        <div class="card-footer">
            <a href="{{ url_for('calendar.upcoming') }}" class="btn btn-outline-primary btn-sm w-100">
                <i class="fas fa-calendar-week me-1"></i>查看更多日程
            </a>
        </div>
    </div>
</div>

三、技术实现细节

1. CKEditor 5集成

CKEditor 5是一个现代化的富文本编辑器,我们通过以下步骤集成到博客系统中:

  1. 在模板中引入CKEditor 5的CDN:
<script src="https://cdn.ckeditor.com/ckeditor5/35.1.0/classic/ckeditor.js"></script>
  1. 初始化编辑器并处理表单提交:
ClassicEditor
    .create(document.querySelector('#editor'))
    .then(newEditor => {
        editor = newEditor;
    })
    .catch(error => {
        console.error(error);
    });

const form = document.getElementById('postForm');
const contentTextarea = document.querySelector('#editor');

form.addEventListener('submit', function(e) {
    e.preventDefault();
    
    // 获取CKEditor内容
    const editorData = editor.getData();
    
    // 检查内容是否为空
    if (!editorData.trim()) {
        alert('文章内容不能为空!');
        return;
    }
    
    // 将CKEditor内容设置到原始textarea
    contentTextarea.value = editorData;
    
    // 提交表单
    this.submit();
});

2. 嵌套评论实现

嵌套评论功能通过以下数据模型和查询实现:

class Comment(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    content = db.Column(db.Text, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
    parent_id = db.Column(db.Integer, db.ForeignKey('comment.id'))
    
    # 关系
    user = db.relationship('User', backref=db.backref('comments', lazy=True))
    replies = db.relationship('Comment', backref=db.backref('parent', remote_side=[id]), lazy=True)

在视图函数中,我们获取顶级评论和它们的回复:

@bp.route('/view/<int:post_id>')
def view(post_id):
    post = Post.query.get_or_404(post_id)
    
    # 获取顶级评论(没有父评论的评论)
    top_level_comments = Comment.query.filter_by(post_id=post_id, parent_id=None).order_by(Comment.created_at.desc()).all()
    
    # 创建评论表单和回复表单
    comment_form = CommentForm()
    reply_form = ReplyForm()
    
    # 检查当前用户是否已点赞
    has_liked = False
    if current_user.is_authenticated:
        has_liked = Like.query.filter_by(user_id=current_user.id, post_id=post_id).first() is not None
    
    return render_template('blog/view.html', 
                          post=post, 
                          top_level_comments=top_level_comments,
                          comment_form=comment_form,
                          reply_form=reply_form,
                          has_liked=has_liked)

3. 日程管理系统优化

日程管理系统的优化主要涉及以下几个方面:

  1. 统一使用Schedule模型,避免数据不一致
  2. 修改查询逻辑,确保正确显示日程
  3. 改进前端交互,提升用户体验

关键代码示例:

# 获取未来30天内的日程
schedules = Schedule.query.filter(
    Schedule.user_id == current_user.id,
    Schedule.start_time >= datetime.combine(today, time.min),
    Schedule.start_time <= datetime.combine(next_month, time.max)
).order_by(Schedule.start_time).all()

4. 现代JavaScript事件处理

我们使用现代JavaScript事件处理方式改进了交互体验:

document.addEventListener('DOMContentLoaded', function() {
    const deleteButtons = document.querySelectorAll('.delete-btn');
    deleteButtons.forEach(button => {
        button.addEventListener('click', function(e) {
            e.preventDefault();
            if (confirm('确定要删除这个日程吗?此操作不可撤销。')) {
                window.location.href = this.getAttribute('data-delete-url');
            }
        });
    });
});

在这里插入图片描述

四、总结

在V1.1和V1.2版本中,我们对家庭网站进行了全面升级和优化:

  1. V1.1版本添加了完整的博客系统,包括:

    • 文章创建、编辑和删除功能
    • CKEditor 5富文本编辑器集成
    • 评论系统,支持嵌套回复
    • 点赞功能
    • 用户博客页面
  2. V1.2版本优化了日程管理系统,包括:

    • 统一日程模型,解决数据不一致问题
    • 改进日历视图,移除月份限制
    • 优化首页"即将到来的事件"显示
    • 扩展"即将到来的日程"页面时间范围
    • 修复jQuery加载问题
    • 改进日程删除功能

这些功能的添加和优化使我们的家庭网站更加完善,为家庭成员提供了更丰富的交流和记录方式。博客系统让家庭成员可以分享生活点滴和心得体会,评论和点赞功能增强了互动性。日程管理系统的优化则提高了用户体验,使日程安排更加便捷和可靠。

在下一篇文章中,我们将继续探索如何进一步扩展家庭网站的功能,包括添加家庭成员管理、家庭财务管理、家庭聊天功能等,敬请期待!

相关文章:

  • Leetcode-131.Palindrome Partitioning [C++][Java]
  • RUOYI框架在实际项目中的应用三:Ruoyi微服务版本-RuoYi-Cloud
  • JAVA数据库技术(一)
  • Deepseek学习--工具篇之Ollama
  • 基于C#的以太网通讯实现:TcpClient异步通讯详解
  • 设置echarts legend 图例与文字对齐
  • 股指期货有卖不出去的时候吗?
  • 在线 SQL 转 flask SQLAlchemy 模型
  • ctf web入门知识合集
  • 阿里wan2.1本地部署
  • Webpack总结
  • MySQL配置文件my.cnf详解
  • 抽象工厂模式 (Abstract Factory Pattern)
  • 蓝桥杯专项复习——结构体、输入输出
  • 花生好车:重构汽车新零售生态的破局者
  • HTML5前端第三章节
  • Centos离线安装openssl-devel
  • 【深度学习与大模型基础】第5章-线性相关与生成子空间
  • 音视频缓存数学模型
  • AI-医学影像分割方法与流程
  • 王楚钦球拍受损,乒乓球裁判揭秘大赛球拍检测
  • 国家发改委:系统谋划7方面53项配套举措,推动民营经济促进法落地见效
  • 揭秘拜登退选内幕新书引争议,“垃圾信息在四处传播”?
  • 复旦兼职教授高纪凡首秀,勉励学子“看三十年才能看见使命”
  • 学生靠老干妈下饭、职工餐肉类又多又好?纪委出手整治
  • 盲人不能刷脸认证、营业厅拒人工核验,央媒:别让刷脸困住尊严