《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">«</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内容设置到原始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">«</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 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是一个现代化的富文本编辑器,我们通过以下步骤集成到博客系统中:
- 在模板中引入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内容设置到原始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. 日程管理系统优化
日程管理系统的优化主要涉及以下几个方面:
- 统一使用
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加载问题
- 改进日程删除功能
这些功能的添加和优化使我们的家庭网站更加完善,为家庭成员提供了更丰富的交流和记录方式。博客系统让家庭成员可以分享生活点滴和心得体会,评论和点赞功能增强了互动性。日程管理系统的优化则提高了用户体验,使日程安排更加便捷和可靠。
在下一篇文章中,我们将继续探索如何进一步扩展家庭网站的功能,包括添加家庭成员管理、家庭财务管理、家庭聊天功能等,敬请期待!