十四、实际应用项目案例
在这一部分,我们将通过几个实际的项目案例来展示Python在不同领域的应用。这些案例将帮助你理解如何将前面学到的知识应用到实际项目中。
14.1 数据分析项目:学生成绩分析
在这个项目中,我们将使用Python来分析学生成绩数据,包括数据清洗、数据分析和数据可视化。
# 导入必要的库
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['WenQuanYi Zen Hei']
plt.rcParams['axes.unicode_minus'] = False
sns.set(style='whitegrid', font='WenQuanYi Zen Hei', rc={'axes.unicode_minus': False})
# 1. 数据准备
# 创建模拟数据
np.random.seed(42)
num_students = 100
# 生成学生数据
students = []
for i in range(1, num_students + 1):
student_id = f"S{i:03d}"
name = f"学生{i}"
gender = np.random.choice(['男', '女'])
age = np.random.randint(18, 22)
major = np.random.choice(['计算机科学', '数学', '物理', '化学', '生物'])
# 生成成绩数据
math = np.random.normal(75, 10)
physics = np.random.normal(70, 12)
chemistry = np.random.normal(72, 11)
biology = np.random.normal(68, 13)
english = np.random.normal(80, 8)
# 确保成绩在0-100之间
math = max(0, min(100, math))
physics = max(0, min(100, physics))
chemistry = max(0, min(100, chemistry))
biology = max(0, min(100, biology))
english = max(0, min(100, english))
# 计算总分和平均分
total_score = math + physics + chemistry + biology + english
average_score = total_score / 5
# 确定等级
if average_score >= 90:
grade = 'A'
elif average_score >= 80:
grade = 'B'
elif average_score >= 70:
grade = 'C'
elif average_score >= 60:
grade = 'D'
else:
grade = 'F'
students.append({
'学号': student_id,
'姓名': name,
'性别': gender,
'年龄': age,
'专业': major,
'数学': math,
'物理': physics,
'化学': chemistry,
'生物': biology,
'英语': english,
'总分': total_score,
'平均分': average_score,
'等级': grade
})
# 创建DataFrame
df = pd.DataFrame(students)
# 保存数据到CSV文件
df.to_csv('/home/wuying/autoglm/session_a2dc1bc4-2ea2-49d3-a5db-90c5e4232fd4/student_scores.csv', index=False, encoding='utf-8')
# 2. 数据探索
# 显示数据的前几行
print("数据前5行:")
print(df.head())
# 显示数据的基本信息
print("\n数据基本信息:")
print(df.info())
# 显示数据的统计描述
print("\n数据统计描述:")
print(df.describe())
# 3. 数据清洗
# 检查缺失值
print("\n缺失值检查:")
print(df.isnull().sum())
# 检查重复值
print("\n重复值检查:")
print(df.duplicated().sum())
# 4. 数据分析
# 按性别分析
print("\n按性别分析:")
gender_stats = df.groupby('性别').agg({
'数学': ['mean', 'std'],
'物理': ['mean', 'std'],
'化学': ['mean', 'std'],
'生物': ['mean', 'std'],
'英语': ['mean', 'std'],
'平均分': ['mean', 'std']
})
print(gender_stats)
# 按专业分析
print("\n按专业分析:")
major_stats = df.groupby('专业').agg({
'数学': ['mean', 'std'],
'物理': ['mean', 'std'],
'化学': ['mean', 'std'],
'生物': ['mean', 'std'],
'英语': ['mean', 'std'],
'平均分': ['mean', 'std']
})
print(major_stats)
# 按等级分析
print("\n按等级分析:")
grade_counts = df['等级'].value_counts()
print(grade_counts)
# 5. 数据可视化
# 按性别和专业的平均分比较
plt.figure(figsize=(12, 6))
sns.barplot(x='专业', y='平均分', hue='性别', data=df)
plt.title('不同专业和性别的平均分比较')
plt.savefig('/home/wuying/autoglm/session_a2dc1bc4-2ea2-49d3-a5db-90c5e4232fd4/gender_major_avg_score.png')
plt.close()
# 各科成绩的分布
plt.figure(figsize=(12, 6))
subjects = ['数学', '物理', '化学', '生物', '英语']
for i, subject in enumerate(subjects):
plt.subplot(2, 3, i+1)
sns.histplot(df[subject], kde=True)
plt.title(f'{subject}成绩分布')
plt.tight_layout()
plt.savefig('/home/wuying/autoglm/session_a2dc1bc4-2ea2-49d3-a5db-90c5e4232fd4/subject_score_distribution.png')
plt.close()
# 等级分布
plt.figure(figsize=(10, 6))
sns.countplot(x='等级', data=df, order=['A', 'B', 'C', 'D', 'F'])
plt.title('学生等级分布')
plt.savefig('/home/wuying/autoglm/session_a2dc1bc4-2ea2-49d3-a5db-90c5e4232fd4/grade_distribution.png')
plt.close()
# 各科成绩的相关性
plt.figure(figsize=(10, 8))
corr = df[['数学', '物理', '化学', '生物', '英语']].corr()
sns.heatmap(corr, annot=True, cmap='coolwarm')
plt.title('各科成绩相关性')
plt.savefig('/home/wuying/autoglm/session_a2dc1bc4-2ea2-49d3-a5db-90c5e4232fd4/subject_correlation.png')
plt.close()
# 6. 高级分析
# 标准化数据
scaler = StandardScaler()
scaled_data = scaler.fit_transform(df[['数学', '物理', '化学', '生物', '英语']])
# 聚类分析
kmeans = KMeans(n_clusters=3, random_state=42)
df['聚类'] = kmeans.fit_predict(scaled_data)
# 可视化聚类结果
plt.figure(figsize=(10, 6))
sns.scatterplot(x='数学', y='物理', hue='聚类', data=df, palette='viridis')
plt.title('学生聚类结果(基于数学和物理成绩)')
plt.savefig('/home/wuying/autoglm/session_a2dc1bc4-2ea2-49d3-a5db-90c5e4232fd4/student_clustering.png')
plt.close()
# 主成分分析
pca = PCA(n_components=2)
principal_components = pca.fit_transform(scaled_data)
df['PC1'] = principal_components[:, 0]
df['PC2'] = principal_components[:, 1]
# 可视化主成分分析结果
plt.figure(figsize=(10, 6))
sns.scatterplot(x='PC1', y='PC2', hue='等级', data=df, palette='viridis')
plt.title('学生成绩主成分分析')
plt.savefig('/home/wuying/autoglm/session_a2dc1bc4-2ea2-49d3-a5db-90c5e4232fd4/student_pca.png')
plt.close()
# 7. 结果解释
# 分析聚类结果
print("\n聚类结果分析:")
cluster_stats = df.groupby('聚类').agg({
'数学': 'mean',
'物理': 'mean',
'化学': 'mean',
'生物': 'mean',
'英语': 'mean',
'平均分': 'mean',
'等级': lambda x: x.value_counts().index[0] # 众数
})
print(cluster_stats)
# 分析主成分
print("\n主成分分析:")
print(f"第一主成分解释的方差比例: {pca.explained_variance_ratio_[0]:.2f}")
print(f"第二主成分解释的方差比例: {pca.explained_variance_ratio_[1]:.2f}")
print(f"两个主成分总共解释的方差比例: {sum(pca.explained_variance_ratio_):.2f}")
# 8. 生成报告
# 创建HTML报告
html_report = """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生成绩分析报告</title>
<style>
body {
font-family: 'WenQuanYi Zen Hei', Arial, sans-serif;
line-height: 1.6;
max-width: 900px;
margin: 0 auto;
padding: 20px;
color: #333;
}
h1, h2, h3 {
color: #2c3e50;
}
h1 {
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}
table {
border-collapse: collapse;
width: 100%;
margin: 20px 0;
}
table, th, td {
border: 1px solid #ddd;
}
th, td {
padding: 12px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
img {
max-width: 100%;
height: auto;
margin: 20px 0;
border: 1px solid #ddd;
}
.note {
background-color: #fffbea;
border-left: 4px solid #f1c40f;
padding: 10px 15px;
margin: 15px 0;
}
.tip {
background-color: #e8f8f5;
border-left: 4px solid #1abc9c;
padding: 10px 15px;
margin: 15px 0;
}
</style>
</head>
<body>
<h1>学生成绩分析报告</h1>
<h2>1. 数据概述</h2>
<p>本报告分析了{}名学生的成绩数据,包括数学、物理、化学、生物和英语五门课程的成绩。</p>
<h2>2. 数据统计</h2>
<h3>2.1 基本统计信息</h3>
<table>
<tr>
<th>统计量</th>
<th>数学</th>
<th>物理</th>
<th>化学</th>
<th>生物</th>
<th>英语</th>
<th>平均分</th>
</tr>
<tr>
<td>平均分</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
</tr>
<tr>
<td>标准差</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
</tr>
<tr>
<td>最低分</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
</tr>
<tr>
<td>最高分</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
<td>{:.2f}</td>
</tr>
</table>
<h3>2.2 等级分布</h3>
<table>
<tr>
<th>等级</th>
<th>人数</th>
<th>百分比</th>
</tr>
""".format(num_students,
df['数学'].mean(), df['物理'].mean(), df['化学'].mean(), df['生物'].mean(), df['英语'].mean(), df['平均分'].mean(),
df['数学'].std(), df['物理'].std(), df['化学'].std(), df['生物'].std(), df['英语'].std(), df['平均分'].std(),
df['数学'].min(), df['物理'].min(), df['化学'].min(), df['生物'].min(), df['英语'].min(), df['平均分'].min(),
df['数学'].max(), df['物理'].max(), df['化学'].max(), df['生物'].max(), df['英语'].max(), df['平均分'].max())
for grade in ['A', 'B', 'C', 'D', 'F']:
count = grade_counts.get(grade, 0)
percentage = count / num_students * 100
html_report += f"""
<tr>
<td>{grade}</td>
<td>{count}</td>
<td>{percentage:.1f}%</td>
</tr>
"""
html_report += """
</table>
<h2>3. 数据可视化</h2>
<h3>3.1 不同专业和性别的平均分比较</h3>
<img src="gender_major_avg_score.png" alt="不同专业和性别的平均分比较">
<h3>3.2 各科成绩分布</h3>
<img src="subject_score_distribution.png" alt="各科成绩分布">
<h3>3.3 学生等级分布</h3>
<img src="grade_distribution.png" alt="学生等级分布">
<h3>3.4 各科成绩相关性</h3>
<img src="subject_correlation.png" alt="各科成绩相关性">
<h2>4. 高级分析</h2>
<h3>4.1 学生聚类分析</h3>
<img src="student_clustering.png" alt="学生聚类结果">
<p>通过K均值聚类算法,我们将学生分为3个群体。从图中可以看出,不同群体的学生在数学和物理成绩上有明显差异。</p>
<h3>4.2 主成分分析</h3>
<img src="student_pca.png" alt="学生成绩主成分分析">
<p>通过主成分分析,我们将五门课程的成绩降维到两个主成分,这两个主成分总共解释了{:.2f}%的方差。从图中可以看出,不同等级的学生在主成分空间中有一定的分离。</p>
<h2>5. 结论与建议</h2>
<h3>5.1 主要发现</h3>
<ul>
<li>英语成绩普遍较高,平均分为{:.2f}分。</li>
<li>生物成绩相对较低,平均分为{:.2f}分。</li>
<li>数学和物理成绩之间有较强的正相关性。</li>
<li>不同专业的学生在各科成绩上存在差异。</li>
<li>通过聚类分析,可以将学生分为3个群体,每个群体有不同的成绩特点。</li>
</ul>
<h3>5.2 建议</h3>
<ul>
<li>针对生物成绩较低的问题,可以考虑改进教学方法或增加学习资源。</li>
<li>对于成绩较差的学生群体,可以提供额外的辅导和支持。</li>
<li>利用数学和物理成绩之间的相关性,可以考虑整合这两门课程的教学内容。</li>
<li>根据不同专业的学生成绩特点,可以调整各专业的课程设置和教学重点。</li>
</ul>
<div class="note">
<p>注意:本报告基于模拟数据生成,仅用于演示数据分析流程和方法。</p>
</div>
</body>
</html>
""".format(sum(pca.explained_variance_ratio_),
df['英语'].mean(),
df['生物'].mean())
# 保存HTML报告
with open('/home/wuying/autoglm/session_a2dc1bc4-2ea2-49d3-a5db-90c5e4232fd4/student_scores_report.html', 'w', encoding='utf-8') as f:
f.write(html_report)
print("分析完成,报告已生成。")
14.2 Web应用项目:个人博客系统
在这个项目中,我们将使用Flask框架创建一个简单的个人博客系统,包括文章发布、用户认证和评论功能。
# app.py
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
import os
from functools import wraps
# 创建Flask应用
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# 初始化数据库
db = SQLAlchemy(app)
migrate = Migrate(app, db)
# 定义数据库模型
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
posts = db.relationship('Post', backref='author', lazy=True)
comments = db.relationship('Comment', backref='author', lazy=True)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f'<User {self.username}>'
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)
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)
comments = db.relationship('Comment', backref='post', lazy=True, cascade='all, delete-orphan')
def __repr__(self):
return f'<Post {self.title}>'
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)
def __repr__(self):
return f'<Comment {self.id}>'
# 登录装饰器
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
flash('请先登录', 'danger')
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated_function
# 路由和视图函数
@app.route('/')
def index():
page = request.args.get('page', 1, type=int)
posts = Post.query.order_by(Post.created_at.desc()).paginate(
page=page, per_page=5, error_out=False)
return render_template('index.html', posts=posts)
@app.route('/post/<int:post_id>')
def post_detail(post_id):
post = Post.query.get_or_404(post_id)
comments = Comment.query.filter_by(post_id=post.id).order_by(Comment.created_at.desc()).all()
return render_template('post_detail.html', post=post, comments=comments)
@app.route('/create', methods=['GET', 'POST'])
@login_required
def create_post():
if request.method == 'POST':
title = request.form['title']
content = request.form['content']
if not title or not content:
flash('请填写标题和内容', 'danger')
return render_template('create_post.html')
post = Post(title=title, content=content, user_id=session['user_id'])
db.session.add(post)
db.session.commit()
flash('文章创建成功', 'success')
return redirect(url_for('post_detail', post_id=post.id))
return render_template('create_post.html')
@app.route('/edit/<int:post_id>', methods=['GET', 'POST'])
@login_required
def edit_post(post_id):
post = Post.query.get_or_404(post_id)
if post.user_id != session['user_id']:
flash('你没有权限编辑这篇文章', 'danger')
return redirect(url_for('post_detail', post_id=post.id))
if request.method == 'POST':
title = request.form['title']
content = request.form['content']
if not title or not content:
flash('请填写标题和内容', 'danger')
return render_template('edit_post.html', post=post)
post.title = title
post.content = content
post.updated_at = datetime.utcnow()
db.session.commit()
flash('文章更新成功', 'success')
return redirect(url_for('post_detail', post_id=post.id))
return render_template('edit_post.html', post=post)
@app.route('/delete/<int:post_id>', methods=['POST'])
@login_required
def delete_post(post_id):
post = Post.query.get_or_404(post_id)
if post.user_id != session['user_id']:
flash('你没有权限删除这篇文章', 'danger')
return redirect(url_for('post_detail', post_id=post.id))
db.session.delete(post)
db.session.commit()
flash('文章删除成功', 'success')
return redirect(url_for('index'))
@app.route('/comment/<int:post_id>', methods=['POST'])
@login_required
def add_comment(post_id):
post = Post.query.get_or_404(post_id)
content = request.form['content']
if not content:
flash('请填写评论内容', 'danger')
return redirect(url_for('post_detail', post_id=post.id))
comment = Comment(content=content, user_id=session['user_id'], post_id=post.id)
db.session.add(comment)
db.session.commit()
flash('评论添加成功', 'success')
return redirect(url_for('post_detail', post_id=post.id))
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
email = request.form['email']
password = request.form['password']
confirm_password = request.form['confirm_password']
if not username or not email or not password or not confirm_password:
flash('请填写所有字段', 'danger')
return render_template('register.html')
if password != confirm_password:
flash('两次输入的密码不一致', 'danger')
return render_template('register.html')
if User.query.filter_by(username=username).first():
flash('用户名已存在', 'danger')
return render_template('register.html')
if User.query.filter_by(email=email).first():
flash('邮箱已被注册', 'danger')
return render_template('register.html')
user = User(username=username, email=email)
user.set_password(password)
db.session.add(user)
db.session.commit()
flash('注册成功,请登录', 'success')
return redirect(url_for('login'))
return render_template('register.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if not username or not password:
flash('请填写用户名和密码', 'danger')
return render_template('login.html')
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
session['user_id'] = user.id
session['username'] = user.username
flash('登录成功', 'success')
return redirect(url_for('index'))
else:
flash('用户名或密码错误', 'danger')
return render_template('login.html')
@app.route('/logout')
def logout():
session.clear()
flash('已退出登录', 'info')
return redirect(url_for('index'))
@app.route('/profile')
@login_required
def profile():
user = User.query.get(session['user_id'])
posts = Post.query.filter_by(user_id=user.id).order_by(Post.created_at.desc()).all()
return render_template('profile.html', user=user, posts=posts)
# API路由
@app.route('/api/posts')
def api_posts():
posts = Post.query.order_by(Post.created_at.desc()).all()
posts_list = []
for post in posts:
posts_list.append({
'id': post.id,
'title': post.title,
'content': post.content,
'author': post.author.username,
'created_at': post.created_at.isoformat(),
'updated_at': post.updated_at.isoformat()
})
return jsonify(posts_list)
@app.route('/api/post/<int:post_id>')
def api_post(post_id):
post = Post.query.get_or_404(post_id)
comments = Comment.query.filter_by(post_id=post.id).order_by(Comment.created_at.desc()).all()
comments_list = []
for comment in comments:
comments_list.append({
'id': comment.id,
'content': comment.content,
'author': comment.author.username,
'created_at': comment.created_at.isoformat()
})
return jsonify({
'id': post.id,
'title': post.title,
'content': post.content,
'author': post.author.username,
'created_at': post.created_at.isoformat(),
'updated_at': post.updated_at.isoformat(),
'comments': comments_list
})
# 错误处理
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
# 创建数据库表
@app.before_first_request
def create_tables():
db.create_all()
# 启动应用
if __name__ == '__main__':
app.run(debug=True)
下面是博客系统的HTML模板文件:
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}个人博客{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
font-family: 'WenQuanYi Zen Hei', Arial, sans-serif;
background-color: #f8f9fa;
}
.navbar-brand {
font-weight: bold;
}
.card {
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card-title {
color: #2c3e50;
}
.comment {
border-left: 3px solid #3498db;
padding-left: 15px;
margin-bottom: 15px;
}
.comment-meta {
font-size: 0.8rem;
color: #6c757d;
}
footer {
margin-top: 50px;
padding: 20px 0;
background-color: #343a40;
color: white;
text-align: center;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">个人博客</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('index') }}">首页</a>
</li>
</ul>
<ul class="navbar-nav">
{% if session.user_id %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('create_post') }}">写文章</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('profile') }}">个人中心</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout') }}">退出登录</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('login') }}">登录</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('register') }}">注册</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<footer>
<div class="container">
<p>© 2023 个人博客. 保留所有权利.</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
<!-- templates/index.html -->
{% extends "base.html" %}
{% block title %}首页 - 个人博客{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-8">
<h2>最新文章</h2>
{% for post in posts.items %}
<div class="card">
<div class="card-body">
<h5 class="card-title"><a href="{{ url_for('post_detail', post_id=post.id) }}">{{ post.title }}</a></h5>
<p class="card-text">{{ post.content[:200] }}...</p>
<p class="card-text">
<small class="text-muted">
作者: {{ post.author.username }} |
发布时间: {{ post.created_at.strftime('%Y-%m-%d %H:%M') }}
{% if post.updated_at != post.created_at %}
| 更新时间: {{ post.updated_at.strftime('%Y-%m-%d %H:%M') }}
{% endif %}
</small>
</p>
</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('index', page=posts.prev_num) }}">上一页</a>
</li>
{% endif %}
{% for page_num in posts.iter_pages() %}
{% if page_num %}
{% if page_num != posts.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('index', page=page_num) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if posts.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('index', page=posts.next_num) }}">下一页</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
关于博客
</div>
<div class="card-body">
<p>这是一个使用Flask构建的个人博客系统。你可以在这里发布文章、阅读他人的文章并发表评论。</p>
<p>如果你还没有账号,请先<a href="{{ url_for('register') }}">注册</a>一个。</p>
</div>
</div>
</div>
</div>
{% endblock %}
<!-- templates/post_detail.html -->
{% extends "base.html" %}
{% block title %}{{ post.title }} - 个人博客{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-8">
<article>
<h1>{{ post.title }}</h1>
<p class="text-muted">
作者: {{ post.author.username }} |
发布时间: {{ post.created_at.strftime('%Y-%m-%d %H:%M') }}
{% if post.updated_at != post.created_at %}
| 更新时间: {{ post.updated_at.strftime('%Y-%m-%d %H:%M') }}
{% endif %}
</p>
<hr>
<div>
{{ post.content | safe }}
</div>
{% if session.user_id and post.user_id == session.user_id %}
<div class="mt-4">
<a href="{{ url_for('edit_post', post_id=post.id) }}" class="btn btn-primary">编辑</a>
<form method="POST" action="{{ url_for('delete_post', post_id=post.id) }}" class="d-inline" onsubmit="return confirm('确定要删除这篇文章吗?');">
<button type="submit" class="btn btn-danger">删除</button>
</form>
</div>
{% endif %}
</article>
<hr>
<section>
<h3>评论 ({{ comments|length }})</h3>
{% if comments %}
{% for comment in comments %}
<div class="comment">
<div class="comment-content">{{ comment.content }}</div>
<div class="comment-meta">
{{ comment.author.username }} - {{ comment.created_at.strftime('%Y-%m-%d %H:%M') }}
</div>
</div>
{% endfor %}
{% else %}
<p>暂无评论。</p>
{% endif %}
{% if session.user_id %}
<div class="mt-4">
<h4>发表评论</h4>
<form method="POST" action="{{ url_for('add_comment', post_id=post.id) }}">
<div class="mb-3">
<textarea name="content" class="form-control" rows="3" required></textarea>
</div>
<button type="submit" class="btn btn-primary">提交评论</button>
</form>
</div>
{% else %}
<p>请<a href="{{ url_for('login') }}">登录</a>后发表评论。</p>
{% endif %}
</section>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
文章作者
</div>
<div class="card-body">
<h5>{{ post.author.username }}</h5>
<p>注册时间: {{ post.author.created_at.strftime('%Y-%m-%d') }}</p>
<p>文章数: {{ post.author.posts|length }}</p>
<p>评论数: {{ post.author.comments|length }}</p>
</div>
</div>
</div>
</div>
{% endblock %}
<!-- templates/create_post.html -->
{% extends "base.html" %}
{% block title %}写文章 - 个人博客{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-8">
<h2>写文章</h2>
<form method="POST">
<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="mb-3">
<label for="content" class="form-label">内容</label>
<textarea class="form-control" id="content" name="content" rows="10" required></textarea>
</div>
<button type="submit" class="btn btn-primary">发布文章</button>
<a href="{{ url_for('index') }}" class="btn btn-secondary">取消</a>
</form>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
写作提示
</div>
<div class="card-body">
<ul>
<li>标题应该简洁明了,能够概括文章的主要内容。</li>
<li>内容应该结构清晰,段落分明。</li>
<li>可以使用Markdown语法来格式化文本。</li>
<li>发布后,你仍然可以编辑或删除文章。</li>
</ul>
</div>
</div>
</div>
</div>
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const contentTextarea = document.getElementById('content');
const previewDiv = document.createElement('div');
previewDiv.className = 'mt-3 p-3 border rounded';
previewDiv.innerHTML = '<h5>预览</h5><div id="preview-content"></div>';
contentTextarea.parentNode.insertBefore(previewDiv, contentTextarea.nextSibling);
const previewContent = document.getElementById('preview-content');
function updatePreview() {
const markdownText = contentTextarea.value;
const htmlText = marked.parse(markdownText);
previewContent.innerHTML = htmlText;
}
contentTextarea.addEventListener('input', updatePreview);
updatePreview();
});
</script>
{% endblock %}
{% endblock %}
<!-- templates/login.html -->
{% extends "base.html" %}
{% block title %}登录 - 个人博客{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h2>登录</h2>
</div>
<div class="card-body">
<form method="POST">
<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>
<button type="submit" class="btn btn-primary">登录</button>
<a href="{{ url_for('register') }}" class="btn btn-link">还没有账号?注册</a>
</form>
</div>
</div>
</div>
</div>
{% endblock %}