第八章:实战项目
8.4 GUI应用开发
8.4.1 GUI开发基础
-
GUI编程概述
图形用户界面(GUI)使程序更加直观易用,Python提供了多种GUI开发库,如Tkinter、PyQt、wxPython等。
# GUI开发的基本流程 # 1. 创建主窗口 # 2. 添加控件(按钮、标签、输入框等) # 3. 设置布局 # 4. 定义事件处理函数 # 5. 启动主循环
-
Tkinter基础
Tkinter是Python标准库中的GUI工具包,简单易用,适合开发小型应用程序。
import tkinter as tk from tkinter import messagebox # 创建主窗口 root = tk.Tk() root.title("我的第一个GUI应用") root.geometry("400x300") # 设置窗口大小 # 添加标签 label = tk.Label(root, text="你好,这是一个简单的GUI程序", font=("宋体", 14)) label.pack(pady=20) # 添加输入框 entry = tk.Entry(root, width=30) entry.pack(pady=10) # 定义按钮点击事件处理函数 def on_button_click(): name = entry.get() if name: messagebox.showinfo("问候", f"你好,{name}!欢迎使用我的程序。") else: messagebox.showwarning("警告", "请输入你的名字!") # 添加按钮 button = tk.Button(root, text="点击问候", command=on_button_click, bg="#4CAF50", fg="white") button.pack(pady=10) # 启动主循环 root.mainloop()
-
布局管理
GUI程序需要合理安排控件的位置和大小,Tkinter提供了几种布局管理器。
import tkinter as tk root = tk.Tk() root.title("布局管理示例") # Frame是一个容器控件,用于组织其他控件 frame = tk.Frame(root, padx=10, pady=10) frame.pack(padx=20, pady=20) # 使用Grid布局(网格布局) tk.Label(frame, text="用户名:").grid(row=0, column=0, sticky="e", pady=5) tk.Entry(frame).grid(row=0, column=1, pady=5) tk.Label(frame, text="密码:").grid(row=1, column=0, sticky="e", pady=5) password_entry = tk.Entry(frame, show="*") # 密码输入框,显示* password_entry.grid(row=1, column=1, pady=5) # 使用Pack布局(包装布局) button_frame = tk.Frame(frame) button_frame.grid(row=2, column=0, columnspan=2, pady=10) tk.Button(button_frame, text="登录").pack(side="left", padx=5) tk.Button(button_frame, text="取消").pack(side="left", padx=5) # 使用Place布局(绝对定位) status_label = tk.Label(root, text="状态: 就绪", bd=1, relief="sunken", anchor="w") status_label.pack(side="bottom", fill="x") root.mainloop()
-
事件处理
GUI程序通过事件驱动,需要定义各种事件的处理函数。
import tkinter as tk root = tk.Tk() root.title("事件处理示例") # 创建一个画布 canvas = tk.Canvas(root, width=400, height=300, bg="white") canvas.pack(pady=10) # 当前绘图状态 drawing = False last_x, last_y = 0, 0 # 鼠标按下事件处理函数 def on_mouse_down(event): global drawing, last_x, last_y drawing = True last_x, last_y = event.x, event.y # 鼠标移动事件处理函数 def on_mouse_move(event): global drawing, last_x, last_y if drawing: canvas.create_line(last_x, last_y, event.x, event.y, width=2) last_x, last_y = event.x, event.y # 鼠标释放事件处理函数 def on_mouse_up(event): global drawing drawing = False # 清除画布事件处理函数 def clear_canvas(): canvas.delete("all") # 绑定鼠标事件 canvas.bind("<Button-1>", on_mouse_down) # 鼠标左键按下 canvas.bind("<B1-Motion>", on_mouse_move) # 鼠标左键按下并移动 canvas.bind("<ButtonRelease-1>", on_mouse_up) # 鼠标左键释放 # 添加清除按钮 clear_button = tk.Button(root, text="清除画布", command=clear_canvas) clear_button.pack(pady=10) root.mainloop()
8.4.2 待办事项管理器项目
-
项目需求分析
我们将开发一个图形界面的待办事项管理器,允许用户添加、编辑、完成和删除待办事项。
# 待办事项管理器的核心功能 # 1. 添加新待办事项 # 2. 编辑现有待办事项 # 3. 标记待办事项为已完成 # 4. 删除待办事项 # 5. 保存待办事项到文件 # 6. 从文件加载待办事项
-
界面设计
设计一个简洁直观的用户界面,包括待办事项列表、添加和编辑表单等。
import tkinter as tk from tkinter import ttk, messagebox, simpledialog import json import os from datetime import datetime class TodoApp: def __init__(self, root): self.root = root self.root.title("待办事项管理器") self.root.geometry("600x450") self.root.resizable(True, True) # 数据存储 self.todos = [] self.data_file = "todos.json" # 创建界面 self.create_ui() # 加载数据 self.load_todos() def create_ui(self): """创建用户界面""" # 创建主框架 main_frame = ttk.Frame(self.root, padding="10") main_frame.pack(fill=tk.BOTH, expand=True) # 创建标题标签 title_label = ttk.Label(main_frame, text="待办事项管理器", font=("宋体", 16, "bold")) title_label.pack(pady=10) # 创建按钮框架 button_frame = ttk.Frame(main_frame) button_frame.pack(fill=tk.X, pady=5) # 添加按钮 add_button = ttk.Button(button_frame, text="添加待办事项", command=self.add_todo) add_button.pack(side=tk.LEFT, padx=5) edit_button = ttk.Button(button_frame, text="编辑", command=self.edit_todo) edit_button.pack(side=tk.LEFT, padx=5) complete_button = ttk.Button(button_frame, text="标记完成", command=self.toggle_complete) complete_button.pack(side=tk.LEFT, padx=5) delete_button = ttk.Button(button_frame, text="删除", command=self.delete_todo) delete_button.pack(side=tk.LEFT, padx=5) # 创建待办事项列表 list_frame = ttk.Frame(main_frame) list_frame.pack(fill=tk.BOTH, expand=True, pady=10) # 创建滚动条 scrollbar = ttk.Scrollbar(list_frame) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # 创建Treeview控件 columns = ("id", "title", "due_date", "status") self.todo_tree = ttk.Treeview(list_frame, columns=columns, show="headings", yscrollcommand=scrollbar.set) # 设置列标题 self.todo_tree.heading("id", text="ID") self.todo_tree.heading("title", text="标题") self.todo_tree.heading("due_date", text="截止日期") self.todo_tree.heading("status", text="状态") # 设置列宽 self.todo_tree.column("id", width=50) self.todo_tree.column("title", width=300) self.todo_tree.column("due_date", width=100) self.todo_tree.column("status", width=100) # 绑定双击事件 self.todo_tree.bind("<Double-1>", lambda event: self.edit_todo()) # 放置Treeview self.todo_tree.pack(fill=tk.BOTH, expand=True) # 配置滚动条 scrollbar.config(command=self.todo_tree.yview) # 创建状态栏 self.status_var = tk.StringVar() self.status_var.set("就绪") status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) status_bar.pack(side=tk.BOTTOM, fill=tk.X) def load_todos(self): """从文件加载待办事项""" try: if os.path.exists(self.data_file): with open(self.data_file, "r", encoding="utf-8") as f: self.todos = json.load(f) self.update_todo_list() self.status_var.set(f"已加载 {len(self.todos)} 个待办事项") except Exception as e: messagebox.showerror("错误", f"加载数据时出错: {e}") self.todos = [] def save_todos(self): """保存待办事项到文件""" try: with open(self.data_file, "w", encoding="utf-8") as f: json.dump(self.todos, f, ensure_ascii=False, indent=2) self.status_var.set(f"已保存 {len(self.todos)} 个待办事项") except Exception as e: messagebox.showerror("错误", f"保存数据时出错: {e}") def update_todo_list(self): """更新待办事项列表显示""" # 清空列表 for item in self.todo_tree.get_children(): self.todo_tree.delete(item) # 添加待办事项到列表 for todo in self.todos: status = "已完成" if todo.get("completed", False) else "未完成" self.todo_tree.insert("", tk.END, values=( todo.get("id", ""), todo.get("title", ""), todo.get("due_date", ""), status )) def add_todo(self): """添加新待办事项""" # 创建对话框 dialog = tk.Toplevel(self.root) dialog.title("添加待办事项") dialog.geometry("400x200") dialog.resizable(False, False) dialog.transient(self.root) # 设置为主窗口的子窗口 dialog.grab_set() # 模态对话框 # 创建表单 ttk.Label(dialog, text="标题:").grid(row=0, column=0, sticky=tk.W, padx=10, pady=10) title_entry = ttk.Entry(dialog, width=30) title_entry.grid(row=0, column=1, padx=10, pady=10) title_entry.focus_set() # 设置焦点 ttk.Label(dialog, text="描述:").grid(row=1, column=0, sticky=tk.W, padx=10, pady=10) desc_entry = ttk.Entry(dialog, width=30) desc_entry.grid(row=1, column=1, padx=10, pady=10) ttk.Label(dialog, text="截止日期 (YYYY-MM-DD):").grid(row=2, column=0, sticky=tk.W, padx=10, pady=10) due_date_entry = ttk.Entry(dialog, width=30) due_date_entry.grid(row=2, column=1, padx=10, pady=10) due_date_entry.insert(0, datetime.now().strftime("%Y-%m-%d")) # 保存函数 def save_todo(): title = title_entry.get().strip() if not title: messagebox.showwarning("警告", "标题不能为空!") return # 创建新待办事项 todo_id = 1 if self.todos: todo_id = max(todo.get("id", 0) for todo in self.todos) + 1 new_todo = { "id": todo_id, "title": title, "description": desc_entry.get().strip(), "due_date": due_date_entry.get().strip(), "completed": False, "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S") } self.todos.append(new_todo) self.save_todos() self.update_todo_list() dialog.destroy() # 按钮框架 button_frame = ttk.Frame(dialog) button_frame.grid(row=3, column=0, columnspan=2, pady=20) ttk.Button(button_frame, text="保存", command=save_todo).pack(side=tk.LEFT, padx=10) ttk.Button(button_frame, text="取消", command=dialog.destroy).pack(side=tk.LEFT, padx=10) def edit_todo(self): """编辑选中的待办事项""" selected = self.todo_tree.selection() if not selected: messagebox.showinfo("提示", "请先选择一个待办事项") return # 获取选中项的ID item_id = self.todo_tree.item(selected[0], "values")[0] # 查找对应的待办事项 todo = None for t in self.todos: if t.get("id") == int(item_id): todo = t break if not todo: return # 创建对话框 dialog = tk.Toplevel(self.root) dialog.title("编辑待办事项") dialog.geometry("400x200") dialog.resizable(False, False) dialog.transient(self.root) dialog.grab_set() # 创建表单 ttk.Label(dialog, text="标题:").grid(row=0, column=0, sticky=tk.W, padx=10, pady=10) title_entry = ttk.Entry(dialog, width=30) title_entry.grid(row=0, column=1, padx=10, pady=10) title_entry.insert(0, todo.get("title", "")) ttk.Label(dialog, text="描述:").grid(row=1, column=0, sticky=tk.W, padx=10, pady=10) desc_entry = ttk.Entry(dialog, width=30) desc_entry.grid(row=1, column=1, padx=10, pady=10) desc_entry.insert(0, todo.get("description", "")) ttk.Label(dialog, text="截止日期 (YYYY-MM-DD):").grid(row=2, column=0, sticky=tk.W, padx=10, pady=10) due_date_entry = ttk.Entry(dialog, width=30) due_date_entry.grid(row=2, column=1, padx=10, pady=10) due_date_entry.insert(0, todo.get("due_date", "")) # 保存函数 def update_todo(): title = title_entry.get().strip() if not title: messagebox.showwarning("警告", "标题不能为空!") return # 更新待办事项 todo["title"] = title todo["description"] = desc_entry.get().strip() todo["due_date"] = due_date_entry.get().strip() todo["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.save_todos() self.update_todo_list() dialog.destroy() # 按钮框架 button_frame = ttk.Frame(dialog) button_frame.grid(row=3, column=0, columnspan=2, pady=20) ttk.Button(button_frame, text="保存", command=update_todo).pack(side=tk.LEFT, padx=10) ttk.Button(button_frame, text="取消", command=dialog.destroy).pack(side=tk.LEFT, padx=10) def toggle_complete(self): """切换待办事项的完成状态""" selected = self.todo_tree.selection() if not selected: messagebox.showinfo("提示", "请先选择一个待办事项") return # 获取选中项的ID item_id = self.todo_tree.item(selected[0], "values")[0] # 查找对应的待办事项并切换状态 for todo in self.todos: if todo.get("id") == int(item_id): todo["completed"] = not todo.get("completed", False) break self.save_todos() self.update_todo_list() def delete_todo(self): """删除选中的待办事项""" selected = self.todo_tree.selection() if not selected: messagebox.showinfo("提示", "请先选择一个待办事项") return # 确认删除 if not messagebox.askyesno("确认", "确定要删除选中的待办事项吗?"): return # 获取选中项的ID item_id = self.todo_tree.item(selected[0], "values")[0] # 删除待办事项 self.todos = [todo for todo in self.todos if todo.get("id") != int(item_id)] self.save_todos() self.update_todo_list()
-
主程序
最后,我们创建主程序,启动待办事项管理器。
def main(): root = tk.Tk() app = TodoApp(root) root.protocol("WM_DELETE_WINDOW", lambda: (app.save_todos(), root.destroy())) root.mainloop() if __name__ == "__main__": main()
-
项目扩展思路
这个待办事项管理器还可以进一步扩展:
- 添加任务优先级和分类功能
- 实现任务搜索和筛选功能
- 添加提醒功能,在截止日期前提醒用户
- 改进界面设计,添加主题切换功能
- 添加数据统计和可视化功能,如任务完成率统计
8.5 Web应用入门
8.5.1 Web开发基础
-
Web应用架构
Web应用通常由前端(客户端)和后端(服务器端)组成,前端负责用户界面,后端负责业务逻辑和数据处理。
# Web应用的基本架构 # 1. 前端:HTML、CSS、JavaScript # 2. 后端:服务器端语言(如Python)、Web框架 # 3. 数据库:存储应用数据 # 4. Web服务器:处理HTTP请求和响应
-
Flask框架介绍
Flask是Python的一个轻量级Web框架,简单易学,适合开发小型Web应用。
from flask import Flask # 创建Flask应用实例 app = Flask(__name__) # 定义路由和视图函数 @app.route('/') def home(): return "<h1>Hello, Flask!</h1>" # 启动应用 if __name__ == '__main__': app.run(debug=True)
-
路由与视图
Flask使用装饰器定义路由,将URL映射到视图函数。
from flask import Flask app = Flask(__name__) # 基本路由 @app.route('/') def home(): return "<h1>首页</h1>" # 带参数的路由 @app.route('/user/<username>') def show_user(username): return f"<h1>用户: {username}</h1>" # 指定HTTP方法 @app.route('/login', methods=['GET', 'POST']) def login(): return "<h1>登录页面</h1>" if __name__ == '__main__': app.run(debug=True)
-
模板与静态文件
Flask使用Jinja2模板引擎渲染HTML页面,并支持静态文件(如CSS、JavaScript)。
from flask import Flask, render_template app = Flask(__name__) @app.route('/hello/<name>') def hello(name): # 渲染模板,传递变量 return render_template('hello.html', name=name) if __name__ == '__main__': app.run(debug=True)
模板文件(hello.html):
<!DOCTYPE html> <html> <head> <title>Hello Page</title> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> </head> <body> <h1>Hello, {{ name }}!</h1> <p>Welcome to my Flask application.</p> </body> </html>
8.5.2 个人博客系统项目
-
项目需求分析
我们将开发一个简单的个人博客系统,包括文章列表、文章详情、添加和编辑文章等功能。
# 个人博客系统的核心功能 # 1. 显示文章列表 # 2. 显示文章详情 # 3. 添加新文章 # 4. 编辑现有文章 # 5. 删除文章 # 6. 简单的用户认证
-
项目结构设计
设计合理的项目结构,包括模板、静态文件和数据模型等。
blog_app/ ├── app.py # 主应用文件 ├── models.py # 数据模型 ├── static/ # 静态文件 │ ├── css/ # CSS样式 │ │ └── style.css │ └── js/ # JavaScript脚本 │ └── main.js ├── templates/ # HTML模板 │ ├── base.html # 基础模板 │ ├── index.html # 首页模板 │ ├── post.html # 文章详情模板 │ ├── create.html # 创建文章模板 │ └── edit.html # 编辑文章模板 └── instance/ # 实例文件夹 └── blog.db # SQLite数据库
-
数据模型实现
使用SQLite数据库和Flask-SQLAlchemy ORM来管理博客数据。
# models.py from flask_sqlalchemy import SQLAlchemy from datetime import datetime db = SQLAlchemy() 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) def __repr__(self): return f"Post('{self.title}', '{self.created_at}')" class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(20), unique=True, nullable=False) password = db.Column(db.String(60), nullable=False) def __repr__(self): return f"User('{self.username}')"
-
应用主文件
实现Flask应用的主文件,包括路由和视图函数。
# app.py from flask import Flask, render_template, request, redirect, url_for, flash, session from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash import os from datetime import datetime # 创建应用实例 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) # 定义模型 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) class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(20), unique=True, nullable=False) password = db.Column(db.String(60), nullable=False) # 创建数据库表 with app.app_context(): db.create_all() # 添加默认用户 if not User.query.filter_by(username='admin').first(): default_user = User(username='admin', password=generate_password_hash('password')) db.session.add(default_user) db.session.commit() # 首页路由 @app.route('/') def index(): posts = Post.query.order_by(Post.created_at.desc()).all() return render_template('index.html', posts=posts) # 文章详情路由 @app.route('/post/<int:post_id>') def post(post_id): post = Post.query.get_or_404(post_id) return render_template('post.html', post=post) # 创建文章路由 @app.route('/create', methods=['GET', 'POST']) def create(): if 'user_id' not in session: flash('请先登录', 'danger') return redirect(url_for('login')) if request.method == 'POST': title = request.form['title'] content = request.form['content'] if not title or not content: flash('标题和内容不能为空', 'danger') else: post = Post(title=title, content=content) db.session.add(post) db.session.commit() flash('文章创建成功', 'success') return redirect(url_for('index')) return render_template('create.html') # 编辑文章路由 @app.route('/edit/<int:post_id>', methods=['GET', 'POST']) def edit(post_id): if 'user_id' not in session: flash('请先登录', 'danger') return redirect(url_for('login')) post = Post.query.get_or_404(post_id) if request.method == 'POST': title = request.form['title'] content = request.form['content'] if not title or not content: flash('标题和内容不能为空', 'danger') else: post.title = title post.content = content post.updated_at = datetime.utcnow() db.session.commit() flash('文章更新成功', 'success') return redirect(url_for('post', post_id=post.id)) return render_template('edit.html', post=post) # 删除文章路由 @app.route('/delete/<int:post_id>') def delete(post_id): if 'user_id' not in session: flash('请先登录', 'danger') return redirect(url_for('login')) post = Post.query.get_or_404(post_id) db.session.delete(post) db.session.commit() flash('文章已删除', 'success') return redirect(url_for('index')) # 登录路由 @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] user = User.query.filter_by(username=username).first() if user and check_password_hash(user.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.pop('user_id', None) session.pop('username', None) flash('已登出', 'success') return redirect(url_for('index')) # 启动应用 if __name__ == '__main__': app.run(debug=True)
-
模板实现
创建HTML模板,使用Bootstrap框架美化界面。
基础模板(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 rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> </head> <body> <nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4"> <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> {% if session.user_id %} <li class="nav-item"> <a class="nav-link" href="{{ url_for('create') }}">写文章</a> </li> {% endif %} </ul> <ul class="navbar-nav"> {% if session.user_id %} <li class="nav-item"> <span class="nav-link">你好,{{ session.username }}</span> </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> {% endif %} </ul> </div> </div> </nav> <div class="container"> {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} <div class="alert alert-{{ category }}">{{ message }}</div> {% endfor %} {% endif %} {% endwith %} {% block content %}{% endblock %} </div> <footer class="bg-dark text-white text-center py-3 mt-5"> <div class="container"> <p class="mb-0">© 2023 个人博客系统 | 使用Flask构建</p> </div> </footer> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> <script src="{{ url_for('static', filename='js/main.js') }}"></script> </body> </html>
首页模板(index.html):
{% extends 'base.html' %} {% block title %}首页 - 个人博客{% endblock %} {% block content %} <h1 class="mb-4">最新文章</h1> {% if posts %} <div class="row"> {% for post in posts %} <div class="col-md-6 mb-4"> <div class="card h-100"> <div class="card-body"> <h5 class="card-title">{{ post.title }}</h5> <p class="card-text text-muted">{{ post.created_at.strftime('%Y-%m-%d %H:%M') }}</p> <p class="card-text">{{ post.content[:150] }}{% if post.content|length > 150 %}...{% endif %}</p> <a href="{{ url_for('post', post_id=post.id) }}" class="btn btn-primary">阅读全文</a> </div> </div> </div> {% endfor %} </div> {% else %} <div class="alert alert-info">暂无文章,请先添加。</div> {% endif %} {% endblock %}
-
运行与测试
运行Flask应用并测试各项功能。
# 运行应用 if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5000)
要运行应用,执行以下命令:
python app.py
然后在浏览器中访问 http://localhost:5000 即可查看博客系统。
-
项目扩展思路
这个简单的博客系统还可以进一步扩展:
- 添加评论功能
- 实现文章分类和标签
- 添加用户注册功能
- 实现文章搜索功能
- 添加文章访问统计
- 实现文件上传功能,支持图片插入
- 使用Markdown编辑器优化文章编辑体验