Skip to main content

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

image-20260104092445289

在上一节中,我们已经完成了:

  • 页面结构的模板化
  • 左侧边栏的统一管理
  • 班级数据的动态展示

但如果你现在把自己当成一个真实用户,会立刻发现两个问题:

  1. 班级一多,怎么找?
  2. 数据一多,怎么翻?

image-20260104112741094

先说结论:搜索和分页并不难

我第一次在 Django 里实现搜索和分页的时候,说实话也挺慌的:

“是不是要写很复杂的 SQL?” “是不是要自己算页码?”

后来我才发现:

Django 早就帮你把 80% 的事情做好了。

你要做的,只是在正确的位置配置它

一、先从“搜索”开始:我们到底想怎么搜?

先别写代码,先明确需求。

在班级管理中,我们希望:

  • 可以按 班级名称 搜索
  • 也可以按 班级编号 搜索

页面上已经有一个搜索表单了:

  • 一个输入框
  • 一个搜索按钮

当你点击搜索按钮时,发生了什么?

image-20260104112600793

二、搜索的本质:其实就是一次 GET 请求

页面中这个表单,用的是 GET 提交。

这意味着一件非常重要的事:

搜索条件,会直接体现在 URL 上。

比如你在输入框里输入 一年二班,点击搜索后, 你会看到 URL 变成类似这样:

/grades/?search=123

这里有两个关键点:

  • search:来自 input 的 name
  • 一年二班:用户输入的值

image-20260104111231142

只要你能在视图中拿到这个值,搜索这件事,就已经成功一半了。

三、表单 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 设计的精髓之一。

image-20260104111231142

image-20260104111317217

七、分页:当数据一多,你一定需要它

接下来我们解决第二个问题:分页

新手最常见的误解是:

“分页是不是要自己算偏移量?”

答案是:不用。

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_number
  • page_obj.has_next
  • page_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页面,且没有“上一页”,效果如下图。

image-20260104112032379

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

image-20260104112113958

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

image-20260104112312372

到这里,你已经解决了一个“真实系统级问题”

很多 Django 初学者,会停留在:

“我能把数据查出来,但不知道怎么让页面好用。”

如果你完整实现了这一节,其实已经说明:

  • 你会改写 CBV 的核心逻辑
  • 你理解 GET 参数如何驱动页面变化
  • 你已经能做出“数据可控的列表页”

现在,你可以立刻动手做的 3 件事

别急着往下看,先试试这几件事:

  1. 搜索 + 分页一起用,观察 URL 的变化
  2. paginate_by 改成不同的数字看看效果
  3. 思考一下:学生列表、老师列表是不是完全一样的套路?

下一节,我们会继续把这个页面补完整:

  • 新增 / 编辑按钮真正“动起来”
  • URL 如何与按钮绑定
  • CRUD 的最后一环如何闭合

你现在写的,不再是零散的功能, 而是在逐步完成一个完整的教学管理系统