快速搭建一个漂亮的AI家庭网站-相册/时间线/日历/多用户/个性化配色(中)
摘要
在上一篇文章中,我们介绍了如何搭建一个基础的家庭网站(V1.0版本),包含了用户管理、相册管理、时间线和日历等功能。本文将继续深入,详细介绍V1.1和V1.2版本中新增的功能,主要包括博客系统的实现、富文本编辑器的集成、评论和点赞功能,以及日程管理系统的优化。通过这些功能的添加,我们的家庭网站将更加完善,为家庭成员提供更丰富的交流和记录方式。
代码仓 https://github.com/wyg5208/family_website_V1_2
一、V1.1版本新增功能
1. 博客系统
V1.1版本最重要的更新是添加了完整的博客系统,让家庭成员可以发表文章,分享生活点滴和心得体会。
1.1 数据模型设计
首先,我们需要设计博客相关的数据模型,包括文章、评论和点赞:
# app/models.pyclass 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}>'@propertydef comment_count(self):return len(self.comments)@propertydef 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">«</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 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">»</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"><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内容设置到原始textareacontentTextarea.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">«</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 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">»</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"><i class="fas fa-info-circle me-2"></i>{{ user.username }}还没有发表任何文章。
</div>
{% endif %}
{% endblock %}
二、V1.2版本新增功能
1. 日程管理系统优化
V1.2版本主要对日程管理系统进行了优化,解决了数据模型不一致的问题。
1.1 统一日程模型
在之前的版本中,我们有两个不同的模型(Schedule
和CalendarEvent
)来表示日程事件,这导致了数据不一致的问题。在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 12prev_year = year if month > 1 else year - 1next_month = month + 1 if month < 12 else 1next_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是一个现代化的富文本编辑器,我们通过以下步骤集成到博客系统中:
- 在模板中引入CKEditor 5的CDN:
<script src="https://cdn.ckeditor.com/ckeditor5/35.1.0/classic/ckeditor.js"></script>
- 初始化编辑器并处理表单提交:
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内容设置到原始textareacontentTextarea.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 = Falseif current_user.is_authenticated:has_liked = Like.query.filter_by(user_id=current_user.id, post_id=post_id).first() is not Nonereturn 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. 日程管理系统优化
日程管理系统的优化主要涉及以下几个方面:
- 统一使用
Schedule
模型,避免数据不一致 - 修改查询逻辑,确保正确显示日程
- 改进前端交互,提升用户体验
关键代码示例:
# 获取未来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版本中,我们对家庭网站进行了全面升级和优化:
-
V1.1版本添加了完整的博客系统,包括:
- 文章创建、编辑和删除功能
- CKEditor 5富文本编辑器集成
- 评论系统,支持嵌套回复
- 点赞功能
- 用户博客页面
-
V1.2版本优化了日程管理系统,包括:
- 统一日程模型,解决数据不一致问题
- 改进日历视图,移除月份限制
- 优化首页"即将到来的事件"显示
- 扩展"即将到来的日程"页面时间范围
- 修复jQuery加载问题
- 改进日程删除功能
这些功能的添加和优化使我们的家庭网站更加完善,为家庭成员提供了更丰富的交流和记录方式。博客系统让家庭成员可以分享生活点滴和心得体会,评论和点赞功能增强了互动性。日程管理系统的优化则提高了用户体验,使日程安排更加便捷和可靠。
在下一篇文章中,我们将继续探索如何进一步扩展家庭网站的功能,包括添加家庭成员管理、家庭财务管理、家庭聊天功能等,敬请期待!