Django 从 0 到 1 打造完整电商平台:商品列表页实现

0 阅读7分钟

IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在公众号、今日头条持续发布最新文章,助你少走弯路。


上一篇我们搞定了商品分类树和 SPU 详情页的规格切换,商品模块的骨架已经撑起来了。但一个商城不能只靠详情页活着——用户得有地方“逛”,能看到一页一页的商品,像刷货架一样。今天我们就来实现电商的 商品列表页,并且加入经典的分页功能。

需求很明确:所有上架的商品(SKU)以网格形式展示,用户可以通过分类链接进入某个分类下的商品列表,每页展示固定数量,底部有分页导航。全程使用 Django 内置的 Paginator,无需第三方库,简单可靠。


一、需求分析

商品列表页的核心功能:

  1. 默认展示所有上架 SKU,按创建时间倒序排列。

  2. 支持按分类筛选:通过 URL 查询参数 ?category_id=1 过滤该分类及其子分类下的商品。

  3. 分页展示:每页 12 个商品,底部生成 Bootstrap 风格的分页导航。

  4. 商品卡片:显示主图(或占位图)、名称、价格、销量等信息,点击跳转到 SPU 详情页。

  5. 排序暂时留到第 15 篇,这里保持简单。


二、视图实现

我们将使用 Django 的 ListView 来快速构建,但为了更灵活地处理分类筛选(包含子分类),还是采用函数视图 + Paginator 手写。

编辑 apps/products/views.py,在已有内容基础上追加:

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from .models import SKU, Category


def sku_list(request):
    # 获取所有上架 SKU,预加载关联数据,减少数据库查询
    skus = SKU.objects.filter(is_active=True).select_related('spu__category').prefetch_related('images').order_by('-create_time')

    # 分类筛选
    category_id = request.GET.get('category_id')
    if category_id:
        try:
            category = Category.objects.get(pk=category_id, is_active=True)
            # 获取该分类及其所有子分类的 ID 列表
            category_ids = [category.id]
            # 简单处理两级分类:查找子分类
            children = Category.objects.filter(parent=category, is_active=True)
            category_ids.extend(children.values_list('id', flat=True))
            # 过滤出属于这些分类的 SPU 的 SKU
            skus = skus.filter(spu__category_id__in=category_ids)
        except Category.DoesNotExist:
            # 分类不存在时返回空列表
            skus = skus.none()

    # 分页:每页 12 个
    paginator = Paginator(skus, 12)
    page_number = request.GET.get('page', 1)

    try:
        page_obj = paginator.page(page_number)
    except PageNotAnInteger:
        page_obj = paginator.page(1)
    except EmptyPage:
        page_obj = paginator.page(paginator.num_pages)

    # 传递当前分类信息供模板使用
    current_category = None
    if category_id:
        try:
            current_category = Category.objects.get(pk=category_id)
        except Category.DoesNotExist:
            pass

    return render(request, 'products/sku_list.html', {
        'page_obj': page_obj,
        'current_category': current_category,
    })

代码要点:

  • select_related('spu__category') 连表查询 SPU 和分类,避免循环查询。

  • prefetch_related('images') 预取图片,供模板取主图用。

  • 分类筛选时,先取出当前分类及其子分类的 ID,再通过 spu__category_id__in 过滤。

  • Paginatorpage() 方法会自动处理页码越界,我们捕获异常并返回首页或末页。


三、URL 配置

apps/products/urls.py 中添加路由:

urlpatterns = [
    path('categories/', views.category_tree, name='category_tree'),
    path('spu/<int:spu_id>/', views.spu_detail, name='spu_detail'),
    path('list/', views.sku_list, name='sku_list'),   # 新增
]

这样商品列表页的 URL 就是 /products/list/,带分类参数则是 /products/list/?category_id=2


四、模板设计

创建 apps/products/templates/products/sku_list.html

{% extends 'base.html' %}
{% load static %}

{% block title %}
    {% if current_category %}
        {{ current_category.name }} - 商品列表
    {% else %}
        全部商品 - Django 商城
    {% endif %}
{% endblock %}

{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
    <h3>
        {% if current_category %}
            📦 {{ current_category.name }}
        {% else %}
            🛍️ 全部商品
        {% endif %}
    </h3>
    <span class="text-muted">共 {{ page_obj.paginator.count }} 件商品</span>
</div>

<!-- 分类导航(可复用上一篇的分类树链接) -->
<div class="mb-3">
    <a href="{% url 'products:sku_list' %}" class="btn btn-sm {% if not current_category %}btn-primary{% else %}btn-outline-primary{% endif %}">全部</a>
    {% for cat in top_categories %}
        <a href="{% url 'products:sku_list' %}?category_id={{ cat.id }}" class="btn btn-sm {% if current_category.id == cat.id %}btn-primary{% else %}btn-outline-primary{% endif %}">{{ cat.name }}</a>
    {% endfor %}
</div>

<!-- 商品网格 -->
<div class="row">
    {% for sku in page_obj %}
        <div class="col-md-3 col-sm-6 mb-4">
            <div class="card h-100 shadow-sm">
                <a href="{% url 'products:spu_detail' sku.spu.id %}">
                    {% with sku.images.all|first as main_image %}
                        {% if main_image %}
                            <img src="{{ main_image.image.url }}" />
                        {% else %}
                            <img src="{% static 'images/placeholder.png' %}" />
                        {% endif %}
                    {% endwith %}
                </a>
                <div class="card-body d-flex flex-column">
                    <h6 class="card-title">
                        <a href="{% url 'products:spu_detail' sku.spu.id %}" class="text-decoration-none text-dark">
                            {{ sku.name }}
                        </a>
                    </h6>
                    <div class="mt-auto">
                        <span class="fs-5 text-danger fw-bold">¥{{ sku.price }}</span>
                        <span class="text-muted small ms-2">已售 {{ sku.sales }}</span>
                    </div>
                </div>
            </div>
        </div>
    {% empty %}
        <div class="col-12">
            <div class="alert alert-info">该分类下暂无商品。</div>
        </div>
    {% endfor %}
</div>

<!-- 分页导航 -->
{% if page_obj.has_other_pages %}
<nav aria-label="商品列表分页">
    <ul class="pagination justify-content-center">
        <!-- 上一页 -->
        {% if page_obj.has_previous %}
            <li class="page-item">
                <a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if current_category %}&category_id={{ current_category.id }}{% endif %}">« 上一页</a>
            </li>
        {% else %}
            <li class="page-item disabled"><span class="page-link">« 上一页</span></li>
        {% endif %}

        <!-- 页码 -->
        {% for num in page_obj.paginator.page_range %}
            {% if num == page_obj.number %}
                <li class="page-item active"><span class="page-link">{{ num }}</span></li>
            {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
                <li class="page-item">
                    <a class="page-link" href="?page={{ num }}{% if current_category %}&category_id={{ current_category.id }}{% endif %}">{{ num }}</a>
                </li>
            {% endif %}
        {% endfor %}

        <!-- 下一页 -->
        {% if page_obj.has_next %}
            <li class="page-item">
                <a class="page-link" href="?page={{ page_obj.next_page_number }}{% if current_category %}&category_id={{ current_category.id }}{% endif %}">下一页 »</a>
            </li>
        {% else %}
            <li class="page-item disabled"><span class="page-link">下一页 »</span></li>
        {% endif %}
    </ul>
</nav>
{% endif %}
{% endblock %}

模板说明:

  • 在顶部我们使用了一个分类快捷导航条,但这需要传入 top_categories 变量。所以需要修改视图,把顶级分类也传给模板。

更新视图:在 sku_list 中添加:

top_categories = Category.objects.filter(parent__isnull=True, is_active=True).order_by('sort')

然后在 return render 的 context 里加上 'top_categories': top_categories

这样就可以在列表页快速切换分类。


五、视图最终版本(补充 top_categories)

def sku_list(request):
    # ... 之前的 skus 查询和分类筛选 ...

    # 顶级分类,供分类导航使用
    top_categories = Category.objects.filter(parent__isnull=True, is_active=True).order_by('sort')

    # ... 分页逻辑 ...

    return render(request, 'products/sku_list.html', {
        'page_obj': page_obj,
        'current_category': current_category,
        'top_categories': top_categories,
    })

六、添加更多测试数据

为了让分页效果明显,我们需要超过 12 个 SKU。可以在 Admin 中多添加几个,或者编写一个临时命令来快速填充。

apps/products/management/commands/init_product_data.py 中可以追加一些商品,但更简单的是直接在 dbshell 中插入几条记录,不过我们保持规范,可以再创建一个命令 create_test_skus.py 来批量生成。这里提供快捷方式:

在项目根目录执行 python manage.py shell

from products.models import SPU, SKU, Category

# 假设已有电子产品分类下的手机子分类
phone_cat = Category.objects.get(name='手机')
spu_iphone = SPU.objects.get(name='iPhone 15')
spu_samsung = SPU.objects.create(name='Samsung Galaxy S24', brand='Samsung', desc='三星旗舰手机', category=phone_cat)

# 为三星创建几个 SKU
SKU.objects.create(spu=spu_samsung, name='Samsung Galaxy S24 256GB 黑色', specs={'颜色':'黑色','存储':'256GB'}, price=6999, stock=60, is_active=True)
SKU.objects.create(spu=spu_samsung, name='Samsung Galaxy S24 512GB 黑色', specs={'颜色':'黑色','存储':'512GB'}, price=7999, stock=30, is_active=True)

# 为 iPhone 再多加几个颜色
SKU.objects.create(spu=spu_iphone, name='iPhone 15 128GB 星光色', specs={'颜色':'星光色','存储':'128GB'}, price=5999, stock=80, is_active=True)
SKU.objects.create(spu=spu_iphone, name='iPhone 15 256GB 星光色', specs={'颜色':'星光色','存储':'256GB'}, price=6999, stock=40, is_active=True)

现在数据足够了。如果使用之前的初始化命令,可能只有 5 个 SKU,加上手动添加的,总数超过 12,分页效果就出来了。


七、测试流程与输出

启动服务器:

python manage.py runserver

7.1 全部商品列表

访问 http://127.0.0.1:8000/products/list/

页面展示:

  • 顶部显示“🛍️ 全部商品”,共 N 件。

  • 分类快捷按钮:全部(高亮)、电子产品、服装。

  • 商品网格,每行 4 个卡片,显示图片、名称、价格、销量。

  • 底部有分页导航:« 上一页 1 [2] 下一页 »

终端输出:

[23/May/2026 10:15:00] "GET /products/list/ HTTP/1.1" 200 9654

7.2 按分类筛选

点击“电子产品”按钮,或访问 http://127.0.0.1:8000/products/list/?category_id=1(假设电子产品分类 ID=1)。

页面只显示 iPhone 和 Samsung 的 SKU,分类按钮“电子产品”高亮。

终端输出:

[23/May/2026 10:16:12] "GET /products/list/?category_id=1 HTTP/1.1" 200 7632

7.3 分页测试

如果当前商品数量超过 12,底部出现分页。点击“下一页”,URL 变为 ?page=2(若带有 category_id 会保留)。商品卡片内容变化,且分页导航当前页码高亮。

终端输出(点击下一页):

[23/May/2026 10:17:45] "GET /products/list/?page=2 HTTP/1.1" 200 9654

7.4 空分类

访问一个不存在的分类 ID,如 /products/list/?category_id=999,页面显示“该分类下暂无商品。”,总数 0。

终端输出:

[23/May/2026 10:18:33] "GET /products/list/?category_id=999 HTTP/1.1" 200 4512

八、SQL 查询分析(简要)

Django 的 connection.queries 可以在开发环境查看 SQL。但此处不展开,只需要知道通过 select_relatedprefetch_related 我们的查询次数控制在 3~5 次内(分类、SKU 列表、图片),没有 N+1 问题。后续性能优化篇会详解。


九、总结与下集预告

今天我们让商品“上架”到了用户面前,实现了:

  • 商品列表页视图,支持按分类筛选(包含子分类);

  • 利用 Django 内置分页器 Paginator 实现高效分页;

  • 响应式商品卡片布局,展示主图、价格、销量;

  • 分类快捷导航,方便用户浏览。

现在,用户可以浏览全部商品,也能按分类查看,还能跳转到详情页查看规格。但详情页的图片展示还很简陋,下一站我们就来解决这个问题。第 13 篇,我将带你完善 商品详情页与图片展示,包括多图切换、主图高亮、缩略图导航等,让详情页真正“活”起来。保持节奏,明天见!

想了解更多还可以去公众号、今日头条搜索「IT策士」,一起升级 IT 思维 !


本文为《Django 从 0 到 1 打造完整电商平台》系列第 12 篇,作者:IT策士。