IT策士 10余年一线大厂经验专注 IT 思维、架构、职场进阶。我会在公众号、今日头条持续发布最新文章助你少走弯路。上一篇我们实现了商品列表页用户可以浏览分类、翻页查看商品卡片电商的“门面”已经立起来了。但进入商品详情页后图片展示还非常单薄——只显示一张主图完全不足以打动用户。今天我们就来重点打磨商品详情页的图片展示让多图切换、缩略图导航、主图预览一气呵成配上规格联动体验直接拉满。我们的ProductImage模型早已就位只是还没被充分利用。接下来我会带你改造前端让 SKU 切换时图片也跟着变同时实现“点击缩略图切换大图”的效果。一、需求分析商品详情页的图片区需要具备以下能力多图展示一个 SKU 可以有多个图片正面、侧面、细节等主图优先显示。缩略图导航下方显示所有图片的缩略图点击哪张大图就切换为哪张。规格联动用户切换规格如颜色、内存时图片区自动切换到新 SKU 对应的图片组。无图占位如果 SKU 没有任何图片显示默认占位图。响应式布局大图区域和缩略图区域在不同屏幕下都能良好展示。我们直接在第 11 篇的spu_detail.html基础上进行升级尽量用最少的前端代码完成这些需求。二、视图微调——确保图片数据准备就绪打开apps/products/views.py找到spu_detail视图。目前我们已经使用了prefetch_related(skus__images)这会一次性把该 SPU 下所有 SKU 及其图片都加载到内存中无需再改。但为了模板更方便我们可以给每个 SKU 的主图增加一个便捷属性也可以在模板中用 filter 处理这里采用模型方法。编辑apps/products/models.py在SKU模型中添加class SKU(models.Model):# ... 字段省略 ...property def main_image_url(self):获取主图的 URL如果没有图片则返回占位图路径 main_imageself.images.filter(is_mainTrue).first()ifmain_image:returnmain_image.image.url first_imageself.images.first()iffirst_image:returnfirst_image.image.url# 返回静态占位图from django.contrib.staticfiles.storageimportstaticfiles_storagereturnstaticfiles_storage.url(images/placeholder.png)这里用property把主图获取逻辑封装在模型中视图中无需改动。注意你需要确保static/images/placeholder.png存在随便放一张占位图否则 404。然后生成迁移虽然没改数据库但模型方法不需要迁移可以直接用。视图保持不变即可数据已经足够丰富。三、重写商品详情模板我们将完全替换apps/products/templates/products/spu_detail.html的内容。保留原有的规格切换逻辑在此基础上重写图片区。{% extendsbase.html%}{% load static %}{% block title %}{{spu.name}}{% endblock %}{% block content %}divclassrow!-- 图片展示区 --divclasscol-md-5divclasscard shadow-sm mb-3!-- 大图显示 --divclasstext-center p-3idmain-image-containerimgidmain-imagesrc{{ default_sku.main_image_url }}classimg-fluid roundedalt{{ default_sku.name }}stylemax-height: 400px; object-fit: contain;/div!-- 缩略图列表 --divclasspx-3 pb-3divclassd-flex flex-wrap gap-2idthumbnail-list{%forimgindefault_sku.images.all %}imgsrc{{ img.image.url }}classimg-thumbnail thumbnail-img {% if img.is_main %}border-primary{% endif %}stylewidth: 70px; height: 70px; object-fit: cover; cursor: pointer;>{{ img.image.url }}onclickswitchMainImage(this){% empty %}imgsrc{% static images/placeholder.png %}classimg-thumbnail thumbnail-img border-primarystylewidth: 70px; height: 70px; object-fit: cover; cursor: pointer;>{% static images/placeholder.png %}onclickswitchMainImage(this){% endfor %}/div/div/div/div!-- 商品信息区与之前类似增加了规格联动更新图片的逻辑 --divclasscol-md-7h2classfw-bold{{spu.name}}/h2{%ifspu.brand %}pclasstext-muted品牌{{spu.brand}}/p{% endif %}pclasstext-muted{{spu.desc}}/p!-- 价格区 --divclassmy-3spanclassfs-3 text-danger fw-boldidsku-price¥{{default_sku.price}}/span{%ifdefault_sku.cost_price %}spanclasstext-muted text-decoration-line-through ms-2¥{{default_sku.cost_price}}/span{% endif %}/div!-- 库存 --pclassmb-1库存spanidsku-stock{{default_sku.stock}}/span件/ppclassmb-3销量{{default_sku.sales}}件/p!-- 规格选择区 --dividspecs-area{%forkey, valuesinspecs.items %}divclassmb-3labelclassform-label fw-bold{{key}}/labeldivclassbtn-group>{{ key }}{%forvalinvalues %}buttontypebuttonclassbtn btn-outline-secondary spec-btn {% if forloop.first %}active{% endif %}>{{ val }}{{val}}/button{% endfor %}/div/div{% endfor %}/div!-- 数量与购买按钮后续激活 --divclassmt-4buttonclassbtn btn-primary btn-lgidadd-to-cart-btndisabled加入购物车/buttonbuttonclassbtn btn-danger btn-lg ms-2disabled立即购买/button/div/div/div{% endblock %}{% block extra_js %}script// 将 SKU 数据和图片信息传递给前端 const skusData[{%forskuinskus %}{id:{{sku.id}}, specs:{{sku.specs|safe}}, price:{{ sku.price }}, stock:{{sku.stock}}, sales:{{sku.sales}}, main_image_url:{{ sku.main_image_url }}, images:[{%forimginsku.images.all %}{url:{{ img.image.url }}, is_main:{{img.is_main|yesno:true,false}}}{%ifnot forloop.last %},{% endif %}{% endfor %}]}{%ifnot forloop.last %},{% endif %}{% endfor %}];letselectedSpecs{};// 初始化默认选中规格 document.querySelectorAll(.spec-btn.active).forEach(btn{const keybtn.closest([data-spec-key]).dataset.specKey;selectedSpecs[key]btn.dataset.specValue;});// 根据选中规格匹配 SKUfunctionfindMatchingSku(){returnskusData.find(sku{returnObject.keys(selectedSpecs).every(key{returnsku.specs[key]selectedSpecs[key];});});}// 更新页面显示价格、库存、图片区functionupdateDisplay(sku){if(!sku)return;// 更新价格和库存 document.getElementById(sku-price).textContent¥ sku.price;document.getElementById(sku-stock).textContentsku.stock;// 更新大图 const mainImgdocument.getElementById(main-image);mainImg.srcsku.main_image_url;// 更新缩略图列表 const thumbnailListdocument.getElementById(thumbnail-list);thumbnailList.innerHTML;if(sku.images.length0){sku.images.forEach(img{const thumbdocument.createElement(img);thumb.srcimg.url;thumb.classNameimg-thumbnail thumbnail-img;if(img.is_main)thumb.classList.add(border-primary);thumb.stylewidth: 70px; height: 70px; object-fit: cover; cursor: pointer;;thumb.dataset.fullUrlimg.url;thumb.onclickfunction(){switchMainImage(this);};thumbnailList.appendChild(thumb);});}else{// 无图片时显示占位图 const placeholderUrl{% static images/placeholder.png %};const thumbdocument.createElement(img);thumb.srcplaceholderUrl;thumb.classNameimg-thumbnail thumbnail-img border-primary;thumb.stylewidth: 70px; height: 70px; object-fit: cover; cursor: pointer;;thumb.dataset.fullUrlplaceholderUrl;thumb.onclickfunction(){switchMainImage(this);};thumbnailList.appendChild(thumb);}}// 点击缩略图切换大图functionswitchMainImage(element){// 切换主图 document.getElementById(main-image).srcelement.dataset.fullUrl;// 移除所有缩略图的主图高亮 document.querySelectorAll(.thumbnail-img).forEach(imgimg.classList.remove(border-primary));// 为当前点击的缩略图添加高亮 element.classList.add(border-primary);}// 绑定规格按钮点击事件 document.querySelectorAll(.spec-btn).forEach(btn{btn.addEventListener(click,function(){const groupthis.closest([data-spec-key]);const keygroup.dataset.specKey;const valuethis.dataset.specValue;// 切换同组按钮的 active 状态 group.querySelectorAll(.spec-btn).forEach(bb.classList.remove(active));this.classList.add(active);// 更新当前选中规格 selectedSpecs[key]value;// 匹配 SKU 并更新 const matchedfindMatchingSku();updateDisplay(matched);});});/script{% endblock %}前端逻辑要点skusData数组里每个 SKU 都带着其所有图片的 URL 和is_main标记。updateDisplay在切换规格时不仅更新价格库存还重绘整个缩略图列表和大图。switchMainImage实现点击缩略图切换大图同时高亮当前选中的缩略图。无图片时自动使用静态占位图。四、确保占位图存在在static/images/目录下放一张placeholder.png随便找张灰色背景图即可尺寸 400x400。如果不想用静态文件也可以在模型方法中返回空字符串前端再判断但用占位图更直观。# 示例创建一个简单的占位图如果没有# 可以下载或自制一个 PNG 图片放置到 static/images/placeholder.png五、测试流程与输出启动服务器python manage.py runserver5.1 访问详情页访问一个有多张图片的 SKU 所在的 SPU比如 iPhone 15SPU ID1。如果还没有给 SKU 添加图片先去 Admin 为iPhone 15 128GB 午夜色上传几张测试图片通过 ProductImage 内联添加并标记一张为主图。Admin 操作进入 SKU 列表点编辑在下方 ProductImage 区域上传 2-3 张图片设置其中一张的is_mainTrue设置排序值。5.2 查看图片展示刷新详情页http://127.0.0.1:8000/products/spu/1/。大图区域显示主图。下方缩略图列表显示所有图片主图的缩略图带有蓝色边框高亮。点击其他缩略图大图立即切换蓝色边框转移到该缩略图。终端输出[24/May/202610:10:10]GET /products/spu/1/ HTTP/1.120013452[24/May/202610:10:10]GET /media/products/2026/05/iphone15_main.jpg HTTP/1.120045321[24/May/202610:10:10]GET /media/products/2026/05/iphone15_side.jpg HTTP/1.1200412305.3 切换规格测试图片联动点击 256GB 按钮如果该 SKU 也有自己的图片组可以到 Admin 添加不同的图片则大图和缩略图都会切换为新 SKU 的图片组。若新 SKU 没有图片则显示占位图确保 placeholder.png 存在。终端输出点击规格切换后没有新图片请求因为图片数据已随 JSON 下发无额外请求说明图片切换在前端完成没有服务器请求。5.4 占位图测试如果某个 SKU 完全没有图片访问它的 SPU 并切换到该规格。大图和缩略图都显示为占位图并且占位图没有切换功能保持一个缩略图。验证给一个 SKU 删除所有图片刷新页面。切换到该 SKU大图变占位图缩略图只有一个占位图图标。六、总结与下集预告今天我们把商品详情页的图片展示提升到了实战水准利用模型main_image_url属性统一获取主图前端通过 JSON 传递所有 SKU 的图片数据实现规格联动切换图片组点击缩略图切换大图UI 反馈清晰无图时自动回退到占位图体验完整。现在用户浏览商品时可以看到多角度的商品图片购物决策体验大幅提升。接下来我们要让用户更轻松地找到商品——第 14 篇将实现商品搜索功能基于 Django ORM 的简单搜索支持按名称、品牌模糊匹配让商城越来越像真的。想了解更多还可以去公众号、今日头条搜索「IT策士」一起升级 IT 思维 本文为《Django 从 0 到 1 打造完整电商平台》系列第 13 篇。
Django 从 0 到 1 打造完整电商平台:商品详情页与图片展示
发布时间:2026/5/25 2:13:19
IT策士 10余年一线大厂经验专注 IT 思维、架构、职场进阶。我会在公众号、今日头条持续发布最新文章助你少走弯路。上一篇我们实现了商品列表页用户可以浏览分类、翻页查看商品卡片电商的“门面”已经立起来了。但进入商品详情页后图片展示还非常单薄——只显示一张主图完全不足以打动用户。今天我们就来重点打磨商品详情页的图片展示让多图切换、缩略图导航、主图预览一气呵成配上规格联动体验直接拉满。我们的ProductImage模型早已就位只是还没被充分利用。接下来我会带你改造前端让 SKU 切换时图片也跟着变同时实现“点击缩略图切换大图”的效果。一、需求分析商品详情页的图片区需要具备以下能力多图展示一个 SKU 可以有多个图片正面、侧面、细节等主图优先显示。缩略图导航下方显示所有图片的缩略图点击哪张大图就切换为哪张。规格联动用户切换规格如颜色、内存时图片区自动切换到新 SKU 对应的图片组。无图占位如果 SKU 没有任何图片显示默认占位图。响应式布局大图区域和缩略图区域在不同屏幕下都能良好展示。我们直接在第 11 篇的spu_detail.html基础上进行升级尽量用最少的前端代码完成这些需求。二、视图微调——确保图片数据准备就绪打开apps/products/views.py找到spu_detail视图。目前我们已经使用了prefetch_related(skus__images)这会一次性把该 SPU 下所有 SKU 及其图片都加载到内存中无需再改。但为了模板更方便我们可以给每个 SKU 的主图增加一个便捷属性也可以在模板中用 filter 处理这里采用模型方法。编辑apps/products/models.py在SKU模型中添加class SKU(models.Model):# ... 字段省略 ...property def main_image_url(self):获取主图的 URL如果没有图片则返回占位图路径 main_imageself.images.filter(is_mainTrue).first()ifmain_image:returnmain_image.image.url first_imageself.images.first()iffirst_image:returnfirst_image.image.url# 返回静态占位图from django.contrib.staticfiles.storageimportstaticfiles_storagereturnstaticfiles_storage.url(images/placeholder.png)这里用property把主图获取逻辑封装在模型中视图中无需改动。注意你需要确保static/images/placeholder.png存在随便放一张占位图否则 404。然后生成迁移虽然没改数据库但模型方法不需要迁移可以直接用。视图保持不变即可数据已经足够丰富。三、重写商品详情模板我们将完全替换apps/products/templates/products/spu_detail.html的内容。保留原有的规格切换逻辑在此基础上重写图片区。{% extendsbase.html%}{% load static %}{% block title %}{{spu.name}}{% endblock %}{% block content %}divclassrow!-- 图片展示区 --divclasscol-md-5divclasscard shadow-sm mb-3!-- 大图显示 --divclasstext-center p-3idmain-image-containerimgidmain-imagesrc{{ default_sku.main_image_url }}classimg-fluid roundedalt{{ default_sku.name }}stylemax-height: 400px; object-fit: contain;/div!-- 缩略图列表 --divclasspx-3 pb-3divclassd-flex flex-wrap gap-2idthumbnail-list{%forimgindefault_sku.images.all %}imgsrc{{ img.image.url }}classimg-thumbnail thumbnail-img {% if img.is_main %}border-primary{% endif %}stylewidth: 70px; height: 70px; object-fit: cover; cursor: pointer;>{{ img.image.url }}onclickswitchMainImage(this){% empty %}imgsrc{% static images/placeholder.png %}classimg-thumbnail thumbnail-img border-primarystylewidth: 70px; height: 70px; object-fit: cover; cursor: pointer;>{% static images/placeholder.png %}onclickswitchMainImage(this){% endfor %}/div/div/div/div!-- 商品信息区与之前类似增加了规格联动更新图片的逻辑 --divclasscol-md-7h2classfw-bold{{spu.name}}/h2{%ifspu.brand %}pclasstext-muted品牌{{spu.brand}}/p{% endif %}pclasstext-muted{{spu.desc}}/p!-- 价格区 --divclassmy-3spanclassfs-3 text-danger fw-boldidsku-price¥{{default_sku.price}}/span{%ifdefault_sku.cost_price %}spanclasstext-muted text-decoration-line-through ms-2¥{{default_sku.cost_price}}/span{% endif %}/div!-- 库存 --pclassmb-1库存spanidsku-stock{{default_sku.stock}}/span件/ppclassmb-3销量{{default_sku.sales}}件/p!-- 规格选择区 --dividspecs-area{%forkey, valuesinspecs.items %}divclassmb-3labelclassform-label fw-bold{{key}}/labeldivclassbtn-group>{{ key }}{%forvalinvalues %}buttontypebuttonclassbtn btn-outline-secondary spec-btn {% if forloop.first %}active{% endif %}>{{ val }}{{val}}/button{% endfor %}/div/div{% endfor %}/div!-- 数量与购买按钮后续激活 --divclassmt-4buttonclassbtn btn-primary btn-lgidadd-to-cart-btndisabled加入购物车/buttonbuttonclassbtn btn-danger btn-lg ms-2disabled立即购买/button/div/div/div{% endblock %}{% block extra_js %}script// 将 SKU 数据和图片信息传递给前端 const skusData[{%forskuinskus %}{id:{{sku.id}}, specs:{{sku.specs|safe}}, price:{{ sku.price }}, stock:{{sku.stock}}, sales:{{sku.sales}}, main_image_url:{{ sku.main_image_url }}, images:[{%forimginsku.images.all %}{url:{{ img.image.url }}, is_main:{{img.is_main|yesno:true,false}}}{%ifnot forloop.last %},{% endif %}{% endfor %}]}{%ifnot forloop.last %},{% endif %}{% endfor %}];letselectedSpecs{};// 初始化默认选中规格 document.querySelectorAll(.spec-btn.active).forEach(btn{const keybtn.closest([data-spec-key]).dataset.specKey;selectedSpecs[key]btn.dataset.specValue;});// 根据选中规格匹配 SKUfunctionfindMatchingSku(){returnskusData.find(sku{returnObject.keys(selectedSpecs).every(key{returnsku.specs[key]selectedSpecs[key];});});}// 更新页面显示价格、库存、图片区functionupdateDisplay(sku){if(!sku)return;// 更新价格和库存 document.getElementById(sku-price).textContent¥ sku.price;document.getElementById(sku-stock).textContentsku.stock;// 更新大图 const mainImgdocument.getElementById(main-image);mainImg.srcsku.main_image_url;// 更新缩略图列表 const thumbnailListdocument.getElementById(thumbnail-list);thumbnailList.innerHTML;if(sku.images.length0){sku.images.forEach(img{const thumbdocument.createElement(img);thumb.srcimg.url;thumb.classNameimg-thumbnail thumbnail-img;if(img.is_main)thumb.classList.add(border-primary);thumb.stylewidth: 70px; height: 70px; object-fit: cover; cursor: pointer;;thumb.dataset.fullUrlimg.url;thumb.onclickfunction(){switchMainImage(this);};thumbnailList.appendChild(thumb);});}else{// 无图片时显示占位图 const placeholderUrl{% static images/placeholder.png %};const thumbdocument.createElement(img);thumb.srcplaceholderUrl;thumb.classNameimg-thumbnail thumbnail-img border-primary;thumb.stylewidth: 70px; height: 70px; object-fit: cover; cursor: pointer;;thumb.dataset.fullUrlplaceholderUrl;thumb.onclickfunction(){switchMainImage(this);};thumbnailList.appendChild(thumb);}}// 点击缩略图切换大图functionswitchMainImage(element){// 切换主图 document.getElementById(main-image).srcelement.dataset.fullUrl;// 移除所有缩略图的主图高亮 document.querySelectorAll(.thumbnail-img).forEach(imgimg.classList.remove(border-primary));// 为当前点击的缩略图添加高亮 element.classList.add(border-primary);}// 绑定规格按钮点击事件 document.querySelectorAll(.spec-btn).forEach(btn{btn.addEventListener(click,function(){const groupthis.closest([data-spec-key]);const keygroup.dataset.specKey;const valuethis.dataset.specValue;// 切换同组按钮的 active 状态 group.querySelectorAll(.spec-btn).forEach(bb.classList.remove(active));this.classList.add(active);// 更新当前选中规格 selectedSpecs[key]value;// 匹配 SKU 并更新 const matchedfindMatchingSku();updateDisplay(matched);});});/script{% endblock %}前端逻辑要点skusData数组里每个 SKU 都带着其所有图片的 URL 和is_main标记。updateDisplay在切换规格时不仅更新价格库存还重绘整个缩略图列表和大图。switchMainImage实现点击缩略图切换大图同时高亮当前选中的缩略图。无图片时自动使用静态占位图。四、确保占位图存在在static/images/目录下放一张placeholder.png随便找张灰色背景图即可尺寸 400x400。如果不想用静态文件也可以在模型方法中返回空字符串前端再判断但用占位图更直观。# 示例创建一个简单的占位图如果没有# 可以下载或自制一个 PNG 图片放置到 static/images/placeholder.png五、测试流程与输出启动服务器python manage.py runserver5.1 访问详情页访问一个有多张图片的 SKU 所在的 SPU比如 iPhone 15SPU ID1。如果还没有给 SKU 添加图片先去 Admin 为iPhone 15 128GB 午夜色上传几张测试图片通过 ProductImage 内联添加并标记一张为主图。Admin 操作进入 SKU 列表点编辑在下方 ProductImage 区域上传 2-3 张图片设置其中一张的is_mainTrue设置排序值。5.2 查看图片展示刷新详情页http://127.0.0.1:8000/products/spu/1/。大图区域显示主图。下方缩略图列表显示所有图片主图的缩略图带有蓝色边框高亮。点击其他缩略图大图立即切换蓝色边框转移到该缩略图。终端输出[24/May/202610:10:10]GET /products/spu/1/ HTTP/1.120013452[24/May/202610:10:10]GET /media/products/2026/05/iphone15_main.jpg HTTP/1.120045321[24/May/202610:10:10]GET /media/products/2026/05/iphone15_side.jpg HTTP/1.1200412305.3 切换规格测试图片联动点击 256GB 按钮如果该 SKU 也有自己的图片组可以到 Admin 添加不同的图片则大图和缩略图都会切换为新 SKU 的图片组。若新 SKU 没有图片则显示占位图确保 placeholder.png 存在。终端输出点击规格切换后没有新图片请求因为图片数据已随 JSON 下发无额外请求说明图片切换在前端完成没有服务器请求。5.4 占位图测试如果某个 SKU 完全没有图片访问它的 SPU 并切换到该规格。大图和缩略图都显示为占位图并且占位图没有切换功能保持一个缩略图。验证给一个 SKU 删除所有图片刷新页面。切换到该 SKU大图变占位图缩略图只有一个占位图图标。六、总结与下集预告今天我们把商品详情页的图片展示提升到了实战水准利用模型main_image_url属性统一获取主图前端通过 JSON 传递所有 SKU 的图片数据实现规格联动切换图片组点击缩略图切换大图UI 反馈清晰无图时自动回退到占位图体验完整。现在用户浏览商品时可以看到多角度的商品图片购物决策体验大幅提升。接下来我们要让用户更轻松地找到商品——第 14 篇将实现商品搜索功能基于 Django ORM 的简单搜索支持按名称、品牌模糊匹配让商城越来越像真的。想了解更多还可以去公众号、今日头条搜索「IT策士」一起升级 IT 思维 本文为《Django 从 0 到 1 打造完整电商平台》系列第 13 篇。