6.班级管理的搜索与分页:让列表真正“可用”

在上一节中,我们已经完成了:
- 页面结构的模板化
- 左侧边栏的统一管理
- 班级数据的动态展示
但如果你现在把自己当成一个真实用户,会立刻发现两个问题:
- 班级一多,怎么找?
- 数据一多,怎么翻?

先说结论:搜索和分页并不难
我第一次在 Django 里实现搜索和分页的时候,说实话也挺慌的:
“是不是要写很复杂的 SQL?” “是不是要自己算页码?”
后来我才发现:
Django 早就帮你把 80% 的事情做好了。
你要做的,只是在正确的位置配置它。
一、先从“搜索”开始:我们到底想怎么搜?
先别写代码,先明确需求。
在班级管理中,我们希望:
- 可以按 班级名称 搜索
- 也可以按 班级编号 搜索
页面上已经有一个搜索表单了:
- 一个输入框
- 一个搜索按钮
当你点击搜索按钮时,发生了什么?

二、搜索的本质:其实就是一次 GET 请求
页面中这个表单,用的是 GET 提交。
这意味着一件非常重要的事:
搜索条件,会直接体现在 URL 上。
比如你在输入框里输入 一年二班,点击搜索后,
你会看到 URL 变成类似这样:
/grades/?search=123
这里有两个关键点:
search:来自 input 的name一年二班:用户输入的值

只要你能在视图中拿到这个值,搜索这件事,就已经成功一半了。
三、表单 action 千万别写死,这是一个“经验坑”
在表单中,你可能会看到类似这样的写法:
<form method="get" action="/grades/">
<span>班级名称:</span>
<input type="text" name="search" placeholder="搜索班级名称..." value="">
<input type="submit" value="搜索">
<a href="#">
<button type="button" class="add">新增</button>
</a>
</form>
【代码解析】
-
<form>表单中,method属性是“get”, 表示使用get请求提交表单。 -
<input>标签name属性是"search",那么输入值会添加到URL后面,例如"/grades/?search=123"(这里的123就是用户填写的值)。 -
另一个
<input>标签, 但是它的值并不会显示到URL中,这是因为它的type属性设置为了“submit”,表示作用是提交表单,而不是让用户输入值。
那么,这个表单提交到哪里了呢?提交到action属性指定的位置,也就是“/grades/”,这在功能上是没问题的,但我不推荐你这样写。
原因只有一个:
一旦 URL 发生变化,你要改所有页面。
更稳妥的方式是使用 Django 的 url 模板标签。
例如:
<form action="{% url 'grades_list' %}" method="get">
【代码解析】
-
{% url %}是Django的模版标签,它的主要作用是:通过视图函数/路由的 name 来反向生成 URL,让你的模板完全不用写死 URL 路径。 -
'grades_list'参数对应grades/urls.py中的name属性值。如下:
urlpatterns = [
path('',GradeListView.as_view(),name='grades_list'),
]
这样,无论将来 URL 怎么改,只要别名不变,页面都不用动。
这是写给未来自己的代码。
四、真正的搜索逻辑
现在,所有请求都会回到同一个视图:
GradeListView
而 ListView 本身,早就帮你准备好了一个“入口”。
它内部有一个方法,叫:
get_queryset()
这个方法的职责只有一个:
决定:这一次要从数据库里拿哪些数据。
五、重写 get_queryset
这里是一个非常容易让新手犯错的地方。
我们不是“推翻”父类逻辑,而是:
在父类的基础上,加一点自己的条件。
在 views.py 中这样写:
# 代码位置:grades/views.py
from django.views.generic import ListView
from django.db.models import Q
from .models import Grade
class GradeListView(ListView):
"""
年级列表视图 - 使用 Django 的通用类视图 ListView
负责显示系统中所有的年级信息,并支持简单的搜索功能
"""
# 指定要查询的模型
model = Grade
# 指定使用的模板文件路径
template_name = 'grades/grades_list.html'
# 在模板中使用的上下文变量名称(默认是 object_list)
# 使用更清晰的名称 'grades'
context_object_name = 'grades'
# 排序规则(可选,根据需要可取消注释)
# ordering = ['grade_number'] # 按年级编号排序
def get_queryset(self):
"""
重写 get_queryset 方法来自定义查询集合
主要功能:支持通过 GET 参数 'search' 进行年级名称或年级编号的模糊搜索
"""
# 先获取默认的全部查询集(相当于 Grade.objects.all())
queryset = super().get_queryset()
# 从 URL 的查询参数中获取搜索关键字(?search=xxx)
search = self.request.GET.get('search')
# 如果有搜索关键字,则进行过滤
if search:
# 使用 Q 对象实现「或」条件查询
# icontains:不区分大小写的包含搜索
queryset = queryset.filter(
Q(grade_name__icontains=search) | # 年级名称中包含搜索词
Q(grade_number__icontains=search) # 年级编号中包含搜索词
)
# 返回最终过滤后的查询集
return queryset
我们来拆解一下思路,而不是逐行背代码:
super().get_queryset(): 👉 先拿到“原本该展示的数据”self.request.GET.get('search'): 👉 从 URL 中取搜索关键词Q(...) | Q(...): 👉 表示“或”的并列条件icontains: 👉 包含式搜索,不区分大小写
如果没有搜索条件,就直接返回原始数据。
六、搜索效果验证:这一步一定要亲眼看
刷新页面,试几种情况:
- 搜索:1年2班
- 搜索:2班
- 搜索:班级编号的一部分
你会发现:
数据在变,但视图和模板一行都没动。
这正是 Django 设计的精髓之一。


七、分页:当数据一多,你一定需要它
接下来我们解决第二个问题:分页。
新手最常见的误解是:
“分页是不是要自己算偏移量?”
答案是:不用。
在 ListView 里,你只需要加一个配置。
paginate_by = 10
表示:每一页显示 10 条数据。
为了方便演示,你甚至可以先设成 1,立刻看到效果。代码如下:
class GradeListView(ListView):
# 设置每页显示数量
paginate_by = 1
def get_queryset(self):
pass
八、分页的数据,Django 已经帮你准备好了
只要你启用了分页,Django 会自动往模板中传一个变量:
page_obj
这是一个非常重要的对象。
你不需要记住全部属性,只需要知道几个常用的:
page_obj.number:当前页page_obj.paginator.num_pages:总页数page_obj.has_previous:是否有上一页page_obj.previous_page_numberpage_obj.has_nextpage_obj.next_page_number
九、在模板中实现分页导航
你可以在grades/grades_list.html模板中这样使用:
<!-- 分页导航 -->
<div class="pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">上一页</a>
{% endif %}
<span>
第 {{ page_obj.number }} 页 /
共 {{ page_obj.paginator.num_pages }} 页
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">下一页</a>
{% endif %}
</div>
【代码解析】
- page_obj.has_previous :判断是否有上一页。
- page_obj.has_next:判断是否有下一页。
- page_obj.number: 当前页码
- paginator.num_pages:总页码
刷新页面后,试着点击:
- 上一页
- 下一页
- 首页 / 尾页
你会发现:
分页逻辑完全是“自动运转”的。
数据库中一共4条数据,现在设置为每页显示1条数据,那么将显示4页。默认显示第1页面,且没有“上一页”,效果如下图。

在中间页时,会显示“上一页”和“下一页”,效果如下图。

在最后一页时,将不显示“下一页”,效果如下图。

到这里,你已经解决了一个“真实系统级问题”
很多 Django 初学者,会停留在:
“我能把数据查出来,但不知道怎么让页面好用。”
如果你完整实现了这一节,其实已经说明:
- 你会改写 CBV 的核心逻辑
- 你理解 GET 参数如何驱动页面变化
- 你已经能做出“数据可控的列表页”
现在,你可以立刻动手做的 3 件事
别急着往下看,先试试这几件事:
- 搜索 + 分页一起用,观察 URL 的变化
- 把
paginate_by改成不同的数字看看效果 - 思考一下:学生列表、老师列表是不是完全一样的套路?
下一节,我们会继续把这个页面补完整:
- 新增 / 编辑按钮真正“动起来”
- URL 如何与按钮绑定
- CRUD 的最后一环如何闭合
你现在写的,不再是零散的功能, 而是在逐步完成一个完整的教学管理系统。
【大熊课堂精品课程】
Python零基础入门动画课: https://www.bilibili.com/cheese/play/ss7988
Django+Vue:全栈开发: https://www.bilibili.com/cheese/play/ss8134
PyQT6开发桌面软件: https://www.bilibili.com/cheese/play/ss12314
Python办公自动化: https://www.bilibili.com/cheese/play/ss14990
Cursor AI编程+MCP:零基础实战项目课: https://www.bilibili.com/cheese/play/ss105194189
Pandas数据分析实战: https://www.bilibili.com/cheese/play/ss734522035
AI大模型+Python小白应用实战: https://www.bilibili.com/cheese/play/ss3844