第 6 章 · 分页、搜索与 Admin 定制
本章目标:使用 Paginator 与 ListView.paginate_by 实现商品列表分页;用 Q 对象构建关键词与分类组合搜索;深度定制 Django Admin(list_filter、search_fields、inlines、actions);完成 shop-demo 后台「可运营」的商品管理界面。
学时建议:5~6 小时(含 2 小时跟练)
前置:完成 django-web ch01~ch05(CBV 列表、ORM 关系、Tag 模型)。
6.1 场景说明:商品多了怎么办?
shop-demo 商品数量增长到数百条后:
| 痛点 | 解决方案 | 本章技术 |
|---|---|---|
| 列表一次加载太慢 | 每页 20 条 | Paginator / paginate_by |
| 运营要找某个 SKU | 搜索框 | Q + icontains |
| 批量下架临期商品 | 勾选 + 动作 | Admin actions |
| 编辑商品时顺便改标签 | 同一页内嵌表单 | TabularInline |
Admin 是 Django 自带的运营后台,与面向公众的页面互补。API 网关域名 api.example.com 在 ch07 通过 DRF 暴露,本章聚焦服务端渲染后台。
6.2 ListView 内置分页(推荐)
class ProductListView(LoginRequiredMixin, ListView):
model = Product
template_name = "catalog/product_list.html"
context_object_name = "products"
paginate_by = 20
def get_queryset(self):
return (
Product.objects.filter(is_active=True)
.select_related("category")
.prefetch_related("tags")
.order_by("-created_at")
)
ListView 自动注入模板变量:page_obj、paginator、is_paginated。
6.3 分页模板
catalog/templates/catalog/product_list.html 底部追加:
{% if is_paginated %}
<nav aria-label="分页">
<ul class="pagination">
{% if page_obj.has_previous %}
<li><a href="?page=1{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">首页</a></li>
<li><a href="?page={{ page_obj.previous_page_number }}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">上一页</a></li>
{% endif %}
<li>第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 页(共 {{ paginator.count }} 条)</li>
{% if page_obj.has_next %}
<li><a href="?page={{ page_obj.next_page_number }}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">下一页</a></li>
<li><a href="?page={{ paginator.num_pages }}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">末页</a></li>
{% endif %}
</ul>
</nav>
{% endif %}
分页链接需保留搜索参数 q,否则翻页后搜索条件丢失。
6.4 Q 对象与搜索
6.4.1 Q 语法
from django.db.models import Q
Q(name__icontains="鼠标") | Q(sku__icontains="鼠标") # OR
Q(is_active=True) & Q(stock__gt=0) # AND
~Q(category__slug="digital") # NOT
| 查找类型 | 字段后缀 | 说明 |
|---|---|---|
| 忽略大小写包含 | icontains | 搜索框最常用 |
| 精确匹配 | iexact | SKU 全匹配 |
| 开头匹配 | istartswith | 自动补全 |
| 范围 | gte / lte | 价格区间 |
6.4.2 在 ListView 中实现搜索
class ProductListView(LoginRequiredMixin, ListView):
model = Product
template_name = "catalog/product_list.html"
context_object_name = "products"
paginate_by = 20
def get_queryset(self):
qs = (
Product.objects.select_related("category")
.prefetch_related("tags")
.order_by("-created_at")
)
q = self.request.GET.get("q", "").strip()
category_id = self.request.GET.get("category", "").strip()
if q:
qs = qs.filter(
Q(name__icontains=q)
| Q(sku__icontains=q)
| Q(tags__name__icontains=q)
).distinct()
if category_id.isdigit():
qs = qs.filter(category_id=int(category_id))
status = self.request.GET.get("status", "")
if status == "active":
qs = qs.filter(is_active=True)
elif status == "inactive":
qs = qs.filter(is_active=False)
return qs
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["search_q"] = self.request.GET.get("q", "")
ctx["categories"] = Category.objects.all()
ctx["selected_category"] = self.request.GET.get("category", "")
return ctx
6.4.3 搜索表单模板
列表页顶部:
<form method="get" action="">
<input type="text" name="q" value="{{ search_q }}" placeholder="名称 / SKU / 标签">
<select name="category">
<option value="">全部分类</option>
{% for c in categories %}
<option value="{{ c.pk }}" {% if selected_category == c.pk|stringformat:"s" %}selected{% endif %}>
{{ c.name }}
</option>
{% endfor %}
</select>
<select name="status">
<option value="">全部状态</option>
<option value="active" {% if request.GET.status == "active" %}selected{% endif %}>已上架</option>
<option value="inactive" {% if request.GET.status == "inactive" %}selected{% endif %}>已下架</option>
</select>
<button type="submit">搜索</button>
<a href="{% url 'product_list' %}">重置</a>
</form>
使用 GET 而非 POST:搜索条件体现在 URL,可收藏、可分享。
6.5 Admin 列表定制
catalog/admin.py 完整示例:
from django.contrib import admin
from django.db.models import Count
from .models import Category, Product, Tag