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

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,在已有内容基础上追加:

bash 复制代码
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 过滤。

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


三、URL 配置

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

bash 复制代码
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:

bash 复制代码
{% 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 中添加:

bash 复制代码
top_categories = Category.objects.filter(parent__isnull=True, is_active=True).order_by('sort')

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

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


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

bash 复制代码
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:

bash 复制代码
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,分页效果就出来了。


七、测试流程与输出

启动服务器:

bash 复制代码
python manage.py runserver

7.1 全部商品列表

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

页面展示:

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

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

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

  • 底部有分页导航:<< 上一页 1 2 下一页 >>

终端输出:

bash 复制代码
[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,分类按钮"电子产品"高亮。

终端输出:

bash 复制代码
[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 会保留)。商品卡片内容变化,且分页导航当前页码高亮。

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

bash 复制代码
[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。

终端输出:

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

八、SQL 查询分析(简要)

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


九、总结与下集预告

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

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

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

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

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

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

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


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

相关推荐
金銀銅鐵8 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路9 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x9 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn11 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈12 小时前
AI 读你的提示词,其实很没耐心
后端
用户83562907805113 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我1234513 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒13 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记13 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk5987813 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端