本系列第1-11章节为全栈营JDstore教程,本系列是在全栈营JDstore教程基础之上进行魔改。这是我参赛作品的复盘记录,部分代码显得有些乱。我会持续完善本系列,希望你能喜欢。如果您在阅读中发现BUG,可反馈至我的邮箱kerzzi@outlook.com
目标
分类和商品展示页面开发
git checkout -b story14# 「从零开始做购物网站」第20章 分类和商品展示页面开发
大家好,本系列文章主要是对我参加全栈营JDstore魔改大赛作品「季风 & MONSOON」的复盘。由于本人也是新手,文章中一定有很多幼稚的错误或代码,希望大家帮忙指出,可反馈至我的邮箱kerzzi@outlook.com,我会持续完善本系列。非常感谢。
本系列是在全栈营JDstore教程基础之上进行魔改,本系列第1-11章节为全栈营JDstore教程,请在完成全栈营JDstore教程的基础上进行测试。
目标
分类和商品展示页面开发
步骤
Step 0:建立新分支
在终端执行 checkout -b story14```。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| ### Step 1: 由于分类内容,在很多页面都要用到。所以先打开category.rb 这个model文件,在其中定义一个方法,保证可以在多个页面中,取得这个一二级分类: ```ruby app/model/category.rb class Category < ApplicationRecord validates :title, presence: { message: "名称不能为空" } validates :title, uniqueness: { message: "名称不能重复" } has_ancestry orphan_strategy: :destroy #删除一级分类时,二级分类自动删除 has_many :products, dependent: :destroy before_validation :correct_ancestry + # 保证可以在多个页面中,取得一二级分类 + def self.grouped_data + self.roots.order("weight desc").inject([]) do |result, parent| + row = [] + row << parent + row << parent.children.order("weight desc") + result << row + end + end private def correct_ancestry self.ancestry = nil if self.ancestry.blank? end end
|
Step 2:
在终端执行g controller categories```。1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ```ruby app/controllers/categories_controller.rb class CategoriesController < ApplicationController def show @categories = Category.grouped_data @category = Category.find(params[:id]) @products = @category.products.onshelf.page(params[:page] || 1).per_page(params[:per_page] || 12) .order("id desc").includes(:main_product_image) end end
|
show页面需要展示商品分类又展示产品列表,所以需要从数据库中找出并提供这2个数据,@category、 @products。
在终端执行app/views/categories/show.html.erb```。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 这个是二级分类的详情页面,在其中加入如下代码: ```ruby app/views/categories/show.html.erb <div class="row"> <div class="col-lg-3"> <%= render 'common/categories' %> </div> <div class="col-lg-9"> <ol class="breadcrumb"> <li><a href="<%= root_path %>">首页</a></li> <li class="active"><%= @category.parent.title %></li> <li class="active"><%= @category.title %></li> </ol> <h1><%= @category.title %></h1> <%= render 'common/products' %> </div> </div>
|
Step 3:简单重构
@categories = Category.grouped_data重复了多次,另外重构后可以便于后续开发。当然重构后使用before_action也是可以的,看个人需求。
打开app/controllers/application_controller.rb,做如下修改
app/controllers/application_controller.rb1 2 3 4 5 6 7 8
| class ApplicationController < ActionController::Base protect_from_forgery with: :exception + before_action :fetch_home_data + def fetch_home_data + @categories = Category.grouped_data + end
|
Step 4:
修改app/controllers/categories_controller.rb
app/controllers/categories_controller.rb1 2 3 4 5 6 7 8 9 10 11 12 13
| class CategoriesController < ApplicationController def show - @categories = Category.grouped_data + fetch_home_data @category = Category.find(params[:id]) @products = @category.products.onshelf.page(params[:page] || 1).per_page(params[:per_page] || 12) .order("id desc").includes(:main_product_image) end end
|
Step 5:
同时修改后台app/controllers/admin/base_controller.rb
app/controllers/admin/base_controller.rb1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Admin::BaseController < ActionController::Base layout 'layouts/admin' before_action :authenticate_user! before_action :admin_required before_action :fetch_home_data def admin_required if !current_user.admin? redirect_to "/", alert: "You are not admin." end end def fetch_home_data @categories = Category.grouped_data end end
|
Step 6:
打开app/controllers/products_controller.rb
app/controllers/products_controller.rb1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class ProductsController < ApplicationController def index @products = Product.onshelf.page(params[:page] || 1).per_page(params[:per_page] || 12) .order("id desc") end def show @product = Product.find(params[:id]) end def add_to_cart @product = Product.find(params[:id]) if !current_cart.products.include?(@product) current_cart.add_product_to_cart(@product) flash[:notice] = "你已成功将 #{@product.title} 加入购物车" else flash[:warning] = "你的购物车内已有此物品" end redirect_to :back end end
|
这里onshelf用了重构,以便后面多次使用。
打开打开app/model/product.rb,加入onshelf这个scope。,加入onshelf这个scope。
app/model/product.rb1 2 3
| before_create :set_default_attrs + scope :onshelf, -> { where(status: Status::On) }
|
Step 7:
打开app/views/products/index.html.erb,做如下修改
app/views/products/index.html.erb1 2 3 4 5 6 7 8
| <div class="row"> <div class="col-lg-12"> <ol class="breadcrumb"> <li class="active">首页</li> </ol> <%= render 'common/products' %> </div> </div>
|
Step 8:建立app/views/common/_categories.html.erb
在终端执行app/views/common/_categories.html.erb```。1 2 3 4 5 6 7 8 9 10 11
| 用来独立的放置商品类别,因为后面会在多个页面使用。 ```ruby app/views/common/_categories.html.erb <ul class="list-group"> <% @categories.each do |group| %> <li class="list-group-item"><%= group.first.title %></li> <% group.last.each do |sub_category| %> <li class="list-group-item"><a href="<%= category_path(sub_category) %>"><%= sub_category.title %></a></li> <% end -%> <% end -%> </ul>
|
Step 9:建立app/views/common/_products.html.erb
在终端执行app/views/common/_products.html.erb```。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ```ruby app/views/common/_products.html.erb <% @products.each do |product| %> <div class="col-xs-6 col-md-3 text-center product-list-style"> <div> <% if product.product_images.present? %> <a href="<%= product_path(product) %>"> <img src=<%= qiniu_image_path(product.main_product_image.image.url) %>></a> <% else %> <%= link_to image_tag("http://placehold.it/200x200&text=No Pic", class: "product-list-style-img img-responsive center-block"), product_path(product), target: "_blank" %> <% end %> </div> <br> <%= link_to(product.title, product_path(product), target: "_blank") %> <br> <p><strong>¥ <%= link_to(product.price, product_path(product), target: "_blank") %> 元</strong></p> </div> <% end %>
|
请注意这里我在取出照片时,没有使用product.product_images.first.image.url(:middle),因为这样会再次查询数据库,而且是对所有图片进行检索,这是非常经典的N+1查询问题,会影响网站的性能。为了避免N+1查询,我们可以在products_controller进行如下修改。
打开app/controllers/products_controller.rb,将index action修改成如下内容。
app/controllers/products_controller.rb1 2 3 4
| def index @products = Product.onshelf.page(params[:page] || 1).per_page(params[:per_page] || 12) .order("id desc").includes(:product_images) end
|
这样在第一次查询数据库时,会同时检索出产品的所有product_images。
Step 10:
但是上一步骤中的设置还是不太好,因为我们在商品迭代输出时并不需要商品的所以图片,仅需要一张即可,为此做如下修改。
在product.rb model中,我们建立产品的一对一关系 main_product_image。
打开app/models/product.rb,增加一对一关系。
app/models/product.rb1 2 3 4
| has_many :product_images, -> { order(weight: 'desc') }, dependent: :destroy + has_one :main_product_image, -> { order(weight: 'desc') }, class_name: :ProductImage
|
这样就可以找到该产品权重最高的一张图片了(即主图片)。这里我使用了1 2 3 4 5 6 7 8
| 同时打开prodcuct controller中,进行相应的修改 ```ruby app/controllers/products_controller.rb class ProductsController < ApplicationController def index @products = Product.onshelf.page(params[:page] || 1).per_page(params[:per_page] || 12) .order("id desc").includes(:main_product_image) end
|
这样在第一次查询数据库时,会同时检索出产品的main_product_image。
Step 11:建立路由
打开config/routes.rb,增加相应的路由。
config/routes.rb1
| + resources :categories, only: [:show]
|
在前端做个简单的样式定义:
app/assets/stylesheets/products.scss1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| /****** 商品列表简单样式 ******/
.list-group-item a { display: block; } .msrp { text-decoration: line-through; } .thumbnail { height: 290px; &.detail { height: auto; } a.title { color: #333; } }
|
Step 12: 美化商品展示页面
打开app/views/products/show.html.erb
app/views/products/show.html.erb1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| <div class="container"> <div class="col-lg-3"> <%= render 'common/categories' %> </div> <div class="col-lg-9"> <ol class="breadcrumb"> <li><a href="<%= root_path %>">所有宝贝</a></li> <li class="active"><%= @product.category.parent.title %></li> <li><a href="<%= category_path @product.category %>"><%= @product.category.title %></a></li> <li class="active"><%= @product.title %></li> </ol>
<div class="preview col-md-7 " style=""> <% @product.product_images.each do |product_image| %> <div class="col-xs-6 col-md-6"> <a href="#" class="thumbnail detail"> <%= image_tag product_image.image.url %> </a> </div> <% end %> </div> <div class="preview col-md-5 " style=""> <h1><%= @product.title %></h1> <ul class="list-unstyled"> <li>商品编号: <%= @product.uuid %></li> <li>库存: <%= @product.quantity %></li> </ul> <h4><span class="msrp">原价: ¥<%= @product.msrp %></span></h4> <h3><strong>现价: ¥<%= @product.price %></strong> </h3> <p>购买: <input type="text" name="quantity" value="1" />件</p>
<div class="pull-right"> <% if @product.quantity.present? && @product.quantity > 0 %> <%= link_to("加入购物车", add_to_cart_product_path(@product), method: :post, class: "btn btn-lg btn-danger") %> <% else %> 已销售一空,无法购买 <% end %> </div> </div> <br /> <br /> <ul class="nav nav-tabs"> <li role="presentation" class="active"> <a href="#tab_default_1" data-toggle="tab">宝贝详情</a> </li> <li> <a href="#tab_default_2" data-toggle="tab">宝贝评价(<%= "0" %>) </a> </li> </ul>
<br /> <div class="tab-content"> <div class="tab-pane active" id="tab_default_1"> <p class="product-description"><%= @product.description.html_safe %></p> <p class="product-description2 text-center">产品展示</p> <div class="row"> <% @product.product_images.each do |product_image| %> <div class="col-xs-12 col-md-12 product-show-detail"> <%= image_tag product_image.image.url %> </div> <% end %> </div> <div class="faq"> <p class="faq-title text-center">常见问题</p> <p class="question">使用什么快递发货?</p> <p class="answer">伴客默认使用顺丰快递发货,配送范围覆盖全国大部分地区(港澳台地区除外)。</p> <p class="question">如何申请退货?</p> <p class="answer"> 1.自收到商品之日起7日内,顾客可申请无忧退货,退款将原路返还,不同的银行处理时间不同,预计1-5个工作日到账;<br /> 2.退货流程:<br /> 确认收货-申请退货-客服审核通过-用户寄回商品-仓库签收验货-退款审核-退款完成;<br /> 3.因MONSOON产生的退货,如质量问题,退货邮费由MONSOON承担,退款完成后钱款将沿原渠道返还。因客户个人原因产生的退货,购买和寄回运费由客户个人承担。<br /> </p> <p class="question">如何开具发票?</p> <p class="answer">如需开具普通发票,请在下单时联系客服办理,我们将虽货物一起快递给您;</p> </div> </div> </div> </div> </div>
|
Step 13:将变更 commit 进去 git 里面。
1 2 3 4 5 6
| git add . git commit -m "完成分类和商品展示页面初步开发" git push origin story14 git checkout master git merge story14 git push origin master
|
本章详解
努力更新中,上班族请理解。。。。。
步骤
Step 1:
由于分类内容,在很多页面都要用到
所以先打开category.rb 这个model文件,在其中定义一个方法,保证可以在多个页面中,取得这个一二级分类:
app/model/category.rb1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Category < ApplicationRecord validates :title, presence: { message: "名称不能为空" } validates :title, uniqueness: { message: "名称不能重复" } has_ancestry orphan_strategy: :destroy has_many :products, dependent: :destroy before_validation :correct_ancestry + + def self.grouped_data + self.roots.order("weight desc").inject([]) do |result, parent| + row = [] + row << parent + row << parent.children.order("weight desc") + result << row + end + end private def correct_ancestry self.ancestry = nil if self.ancestry.blank? end end
|
rails g controller categories
app/controllers/categories_controller.rb1 2 3 4 5 6 7 8 9 10 11 12
| class CategoriesController < ApplicationController def show @categories = Category.grouped_data @category = Category.find(params[:id]) @products = @category.products.onshelf.page(params[:page] || 1).per_page(params[:per_page] || 12) .order("id desc").includes(:main_product_image) end end
|
show页面需要展示商品分类又展示产品列表,所以需要从数据库中找出并提供这2个数据,@category、 @products。
touch app/views/categories/show.html.erb
这个是二级分类的详情页面
这个是二级分类的详情页面1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <div class="row"> <div class="col-lg-3"> <%= render 'common/categories' %> </div> <div class="col-lg-9"> <ol class="breadcrumb"> <li><a href="<%= root_path %>">首页</a></li> <li class="active"><%= @category.parent.title %></li> <li class="active"><%= @category.title %></li> </ol> <h1><%= @category.title %></h1> <%= render 'common/products' %> </div> </div>
|
简单重构,@categories = Category.grouped_data重复了多次,另外重构后可以便于后续开发。当然重构后使用before_action也是可以的,看个人需求。
打开app/controllers/application_controller.rb
app/controllers/application_controller.rb1 2 3 4 5 6 7 8
| class ApplicationController < ActionController::Base protect_from_forgery with: :exception + before_action :fetch_home_data + def fetch_home_data + @categories = Category.grouped_data + end
|
修改app/controllers/categories_controller.rb
app/controllers/categories_controller.rb1 2 3 4 5 6 7 8 9 10 11 12 13
| class CategoriesController < ApplicationController def show - @categories = Category.grouped_data + fetch_home_data @category = Category.find(params[:id]) @products = @category.products.onshelf.page(params[:page] || 1).per_page(params[:per_page] || 12) .order("id desc").includes(:main_product_image) end end
|
同时修改后台app/controllers/admin/base_controller.rb
app/controllers/admin/base_controller.rb1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Admin::BaseController < ActionController::Base layout 'layouts/admin' before_action :authenticate_user! before_action :admin_required before_action :fetch_home_data def admin_required if !current_user.admin? redirect_to "/", alert: "You are not admin." end end def fetch_home_data @categories = Category.grouped_data end end
|
Step 2:
打开app/controllers/products_controller.rb
app/controllers/products_controller.rb1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class ProductsController < ApplicationController def index @products = Product.onshelf.page(params[:page] || 1).per_page(params[:per_page] || 12) .order("id desc") end def show @product = Product.find(params[:id]) end def add_to_cart @product = Product.find(params[:id]) if !current_cart.products.include?(@product) current_cart.add_product_to_cart(@product) flash[:notice] = "你已成功将 #{@product.title} 加入购物车" else flash[:warning] = "你的购物车内已有此物品" end redirect_to :back end end
|
这里onshelf用了重构,以便多次使用。
打开打开app/model/product.rb,加入onshelf这个scope。,加入onshelf这个scope。
app/model/product.rb1 2 3
| before_create :set_default_attrs + scope :onshelf, -> { where(status: Status::On) }
|
Step 3:(这个好像没有这样改)
app/views/products/index.html.erb
app/views/products/index.html.erb1 2 3 4 5 6 7 8
| <div class="row"> <div class="col-lg-12"> <ol class="breadcrumb"> <li class="active">首页</li> </ol> <%= render 'common/products' %> </div> </div>
|
Step 4:
touch app/views/common/_categories.html.erb
用来独立的放置商品类别,因为后面会在多个页面使用。
app/views/common/_categories.html.erb1 2 3 4 5 6 7 8
| <ul class="list-group"> <% @categories.each do |group| %> <li class="list-group-item"><%= group.first.title %></li> <% group.last.each do |sub_category| %> <li class="list-group-item"><a href="<%= category_path(sub_category) %>"><%= sub_category.title %></a></li> <% end -%> <% end -%> </ul>
|
Step 5:
touch app/views/common/_products.html.erb
app/views/common/_products.html.erb1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <% @products.each do |product| %> <div class="col-xs-6 col-md-3 text-center product-list-style"> <div> <% if product.product_images.present? %> <a href="<%= product_path(product) %>"> <img src=<%= qiniu_image_path(product.main_product_image.image.url) %>></a>
<% else %> <%= link_to image_tag("http://placehold.it/200x200&text=No Pic", class: "product-list-style-img img-responsive center-block"), product_path(product), target: "_blank" %> <% end %> </div>
<br> <%= link_to(product.title, product_path(product), target: "_blank") %> <br> <p><strong>¥ <%= link_to(product.price, product_path(product), target: "_blank") %> 元</strong></p> </div> <% end %>
|
请注意这里我在取出照片时,没有使用product.product_images.first.image.url(:middle),因为这样会再次查询数据库,而且是对所有图片进行检索,这是非常经典的N+1查询问题,会影响网站的性能。为了避免N+1查询,我们可以在welcome_controller.rb
打开app/controllers/products_controller.rb
app/controllers/products_controller.rb1 2 3 4
| def index @products = Product.onshelf.page(params[:page] || 1).per_page(params[:per_page] || 12) .order("id desc").includes(:product_images) end
|
Step 6:
这样才第一次查询数据库时,会同时检索出产品的所有product_images。
但这样还是不太好,因为我们在商品迭代输出时并不需要商品的所以图片,仅需要一张即可。
在product.rb model中,我们建立产品的一对一关系 main_product_image
打开app/models/product.rb
app/models/product.rb1 2 3 4
| has_many :product_images, -> { order(weight: 'desc') }, dependent: :destroy + has_one :main_product_image, -> { order(weight: 'desc') }, class_name: :ProductImage
|
这样就可以找到该产品权重最高的一张图片了。
这时我们使用了product.main_product_image.image.url(:middle),
同时打开prodcuct controller中
app/controllers/products_controller.rb1 2 3 4 5
| class ProductsController < ApplicationController def index @products = Product.onshelf.page(params[:page] || 1).per_page(params[:per_page] || 12) .order("id desc").includes(:main_product_image) end
|
这样在第一次查询数据库时,会同时检索出产品的main_product_image,
###Step 7:建立路由
config/routes.rb1
| + resources :categories, only: [:show]
|
关于在前端做个简单的样式定义:
app/assets/stylesheets/products.scss1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| /****** 商品列表简单样式 ******/
.list-group-item a { display: block; } .msrp { text-decoration: line-through; } .thumbnail { height: 290px; &.detail { height: auto; } a.title { color: #333; } }
|
Step 8: 美化商品展示页面
打开app/views/products/show.html.erb
app/views/products/show.html.erb1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| <div class="row"> <div class="col-lg-3"> <%= render 'common/categories' %> </div> <div class="col-lg-9"> <ol class="breadcrumb"> <li><a href="<%= root_path %>">所有宝贝</a></li> <li class="active"><%= @product.category.parent.title %></li> <li><a href="<%= category_path @product.category %>"><%= @product.category.title %></a></li> <li class="active"><%= @product.title %></li> </ol>
<div class="preview col-md-7 " style=""> <% @product.product_images.each do |product_image| %> <div class="col-xs-6 col-md-6"> <a href="#" class="thumbnail detail"> <%= image_tag product_image.image.url %> </a> </div> <% end %> </div> <div class="preview col-md-5 " style=""> <h1><%= @product.title %></h1> <ul class="list-unstyled"> <li>商品编号: <%= @product.uuid %></li> <li>库存: <%= @product.quantity %></li> </ul> <h4><span class="msrp">原价: ¥<%= @product.msrp %></span></h4> <h3><strong>现价: ¥<%= @product.price %></strong> </h3> <p>购买: <input type="text" name="quantity" value="1" />件</p>
<div class="pull-right"> <% if @product.quantity.present? && @product.quantity > 0 %> <%= link_to("加入购物车", add_to_cart_product_path(@product), method: :post, class: "btn btn-lg btn-danger") %> <% else %> 已销售一空,无法购买 <% end %> </div> </div> <br /> <br /> <ul class="nav nav-tabs"> <li role="presentation" class="active"> <a href="#tab_default_1" data-toggle="tab">宝贝详情</a> </li> <li> <a href="#tab_default_2" data-toggle="tab">宝贝评价(<%= "0" %>) </a> </li> </ul>
<br /> <div class="tab-content"> <div class="tab-pane active" id="tab_default_1"> <p class="product-description"><%= @product.description.html_safe %></p> <p class="product-description2 text-center">产品展示</p> <div class="row"> <% @product.product_images.each do |product_image| %> <div class="col-xs-12 col-md-12 product-show-detail"> <%= image_tag product_image.image.url %> </div> <% end %> </div> <div class="faq"> <p class="faq-title text-center">常见问题</p> <p class="question">使用什么快递发货?</p> <p class="answer">伴客默认使用顺丰快递发货,配送范围覆盖全国大部分地区(港澳台地区除外)。</p> <p class="question">如何申请退货?</p> <p class="answer"> 1.自收到商品之日起7日内,顾客可申请无忧退货,退款将原路返还,不同的银行处理时间不同,预计1-5个工作日到账;<br /> 2.退货流程:<br /> 确认收货-申请退货-客服审核通过-用户寄回商品-仓库签收验货-退款审核-退款完成;<br /> 3.因MONSOON产生的退货,如质量问题,退货邮费由MONSOON承担,退款完成后钱款将沿原渠道返还。因客户个人原因产生的退货,购买和寄回运费由客户个人承担。<br /> </p> <p class="question">如何开具发票?</p> <p class="answer">如需开具普通发票,请在下单时联系客服办理,我们将虽货物一起快递给您;</p> </div> </div> </div> </div> </div>
|
Step 9:
git add .
git commit -m “完成分类和商品展示页面初步开发”
git push origin story14
git checkout master
git merge story14
git push origin master
解释