天天看點

商城項目-商品詳情

1.商品詳情

當使用者搜尋到商品,肯定會點選檢視,就會進入商品詳情頁,接下來我們完成商品詳情頁的展示,

1.1.Thymeleaf

在商品詳情頁中,我們會使用到Thymeleaf來渲染頁面,是以需要先了解Thymeleaf的文法。

1.2.商品詳情頁服務

商品詳情浏覽量比較大,并發高,我們會獨立開啟一個微服務,用來展示商品詳情。

1.2.1.建立module

商品的詳情頁服務,命名為:​

​leyou-goods-web​

商城項目-商品詳情

目錄:

商城項目-商品詳情

1.2.2.pom依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>leyou</artifactId>
        <groupId>com.leyou.parent</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.goods</groupId>
    <artifactId>leyou-goods-web</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>com.leyou.item</groupId>
            <artifactId>leyou-item-interface</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>      

1.2.3.編寫啟動類

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LeyouGoodsWebApplication {
    public static void main(String[] args) {
        SpringApplication.run(LeyouGoodsWebApplication.class, args);
    }
}      

1.2.4.application.yml檔案

server:
  port: 8084
spring:
  application:
    name: goods-page
  thymeleaf:
    cache: false
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    lease-renewal-interval-in-seconds: 5 # 每隔5秒發送一次心跳
    lease-expiration-duration-in-seconds: 10 # 10秒不發送就過期
    prefer-ip-address: true
    ip-address: 127.0.0.1
    instance-id: ${spring.application.name}.${server.port}      

1.2.5.頁面模闆

我們從leyou-portal中複制item.html模闆到目前項目resource目錄下的templates中:

商城項目-商品詳情

1.3.頁面跳轉

1.3.1.修改頁面跳轉路徑

首先我們需要修改搜尋結果頁的商品位址,目前所有商品的位址都是:http://www.leyou.com/item.html

商城項目-商品詳情

我們應該跳轉到對應的商品的詳情頁才對。

那麼問題來了:商品詳情頁是一個SKU?還是多個SKU的集合?

商城項目-商品詳情

通過詳情頁的預覽,我們知道它是多個SKU的集合,即SPU。

是以,頁面跳轉時,我們應該攜帶SPU的id資訊。

例如:http://www.leyou.com/item/2314123.html

這裡就采用了路徑占位符的方式來傳遞spu的id,我們打開​

​search.html​

​,修改其中的商品路徑:

商城項目-商品詳情

重新整理頁面後再看:

商城項目-商品詳情

1.3.2.nginx反向代理

接下來,我們要把這個位址指向我們剛剛建立的服務:​

​leyou-goods-web​

​,其端口為8084

我們在nginx.conf中添加一段邏輯:

商城項目-商品詳情

把以/item開頭的請求,代理到我們的8084端口。

1.3.3.編寫跳轉controller

在​

​leyou-goods-web​

​中編寫controller,接收請求,并跳轉到商品詳情頁:

@Controller
@RequestMapping("item")
public class GoodsController {

    /**
     * 跳轉到商品詳情頁
     * @param model
     * @param id
     * @return
     */
    @GetMapping("{id}.html")
    public String toItemPage(Model model, @PathVariable("id")Long id){

        return "item";
    }
}      

1.3.4.測試

啟動​

​leyou-goods-page​

​,點選搜尋頁面商品,看是能夠正常跳轉:

商城項目-商品詳情

現在看到的依然是靜态的資料。我們接下來開始頁面的渲染

1.4.封裝模型資料

首先我們一起來分析一下,在這個頁面中需要哪些資料

我們已知的條件是傳遞來的spu的id,我們需要根據spu的id查詢到下面的資料:

  • spu資訊
  • spu的詳情
  • spu下的所有sku
  • 品牌
  • 商品三級分類
  • 商品規格參數、規格參數組

1.4.1.商品微服務提供接口

1.4.1.1.查詢spu

以上所需資料中,查詢spu的接口目前還沒有,我們需要在商品微服務中提供這個接口:

GoodsApi
/**
 * 根據spu的id查詢spu
 * @param id
 * @return
 */
@GetMapping("spu/{id}")
public Spu querySpuById(@PathVariable("id") Long id);      
GoodsController
@GetMapping("spu/{id}")
public ResponseEntity<Spu> querySpuById(@PathVariable("id") Long id){
    Spu spu = this.goodsService.querySpuById(id);
    if(spu == null){
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
    return ResponseEntity.ok(spu);
}      
GoodsService
public Spu querySpuById(Long id) {
    return this.spuMapper.selectByPrimaryKey(id);
}      

1.4.1.2.查詢規格參數組

我們在頁面展示規格時,需要按組展示:

商城項目-商品詳情

組内有多個參數,為了友善展示。我們提供一個接口,查詢規格組,同時在規格組中持有組内的所有參數。

拓展​

​SpecGroup​

​類:

我們在​

​SpecGroup​

​​中添加一個​

​SpecParam​

​的集合,儲存改組下所有規格參數

@Table(name = "tb_spec_group")
public class SpecGroup {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long cid;

    private String name;

    @Transient
    private List<SpecParam> params; // 該組下的所有規格參數集合
}      

然後提供查詢接口:

SpecificationAPI:
@RequestMapping("spec")
public interface SpecificationApi {
    @GetMapping("groups/{cid}")
    public ResponseEntity<List<SpecGroup>> querySpecGroups(@PathVariable("cid") Long cid);

    @GetMapping("/params")
    public List<SpecParam> querySpecParam(
            @RequestParam(value = "gid", required = false) Long gid,
            @RequestParam(value = "cid", required = false) Long cid,
            @RequestParam(value = "searching", required = false) Boolean searching,
            @RequestParam(value = "generic", required = false) Boolean generic);

    // 查詢規格參數組,及組内參數
    @GetMapping("{cid}")
    List<SpecGroup> querySpecsByCid(@PathVariable("cid") Long cid);

}      
SpecificationController
@GetMapping("{cid}")
public ResponseEntity<List<SpecGroup>> querySpecsByCid(@PathVariable("cid") Long cid){
    List<SpecGroup> list = this.specificationService.querySpecsByCid(cid);
    if(list == null || list.size() == 0){
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
    return ResponseEntity.ok(list);
}      
SpecificationService
public List<SpecGroup> querySpecsByCid(Long cid) {
    // 查詢規格組
    List<SpecGroup> groups = this.querySpecGroups(cid);
    SpecParam param = new SpecParam();
    groups.forEach(g -> {
        // 查詢組内參數
        g.setParams(this.querySpecParams(g.getId(), null, null, null));
    });
    return groups;
}      

在service中,我們調用之前編寫過的方法,查詢規格組,和規格參數,然後封裝傳回。

1.4.2.建立FeignClient

我們在​

​leyou-goods-web​

​服務中,建立FeignClient:

BrandClient:

@FeignClient("item-service")
public interface BrandClient extends BrandApi {
}      

CategoryClient

@FeignClient("item-service")
public interface CategoryClient extends CategoryApi {
}      

GoodsClient:

@FeignClient("item-service")
public interface GoodsClient extends GoodsApi {
}      

SpecificationClient:

@FeignClient(value = "item-service")
public interface SpecificationClient extends SpecificationApi{
}      

1.4.3.封裝資料模型

我們建立一個GoodsService,在裡面來封裝資料模型。

這裡要查詢的資料:

  • SPU
  • SpuDetail
  • SKU集合
  • 商品分類
  • 這裡值需要分類的id和name就夠了,是以我們查詢到以後自己需要封裝資料
  • 品牌
  • 規格組
  • 查詢規格組的時候,把規格組下所有的參數也一并查出,上面提供的接口中已經實作該功能,我們直接調
  • sku的特有規格參數

    有了規格組,為什麼這裡還要查詢?

    因為在SpuDetail中的SpecialSpec中,是以id作為規格參數id作為key,如圖:

商城項目-商品詳情

我們就需要把id和name一一對應起來,是以需要額外查詢sku的特有規格參數,然後變成一個id:name的鍵值對格式。也就是一個Map,友善将來根據id查找!

Service代碼
@Service
public class GoodsService {

    @Autowired
    private GoodsClient goodsClient;

    @Autowired
    private BrandClient brandClient;

    @Autowired
    private CategoryClient categoryClient;

    @Autowired
    private SpecificationClient specificationClient;

    private static final Logger logger = LoggerFactory.getLogger(GoodsService.class);

    public Map<String, Object> loadModel(Long spuId){

        try {
            // 查詢spu
            Spu spu = this.goodsClient.querySpuById(spuId);

            // 查詢spu詳情
            SpuDetail spuDetail = this.goodsClient.querySpuDetailById(spuId);

            // 查詢sku
            List<Sku> skus = this.goodsClient.querySkuBySpuId(spuId);

            // 查詢品牌
            List<Brand> brands = this.brandClient.queryBrandByIds(Arrays.asList(spu.getBrandId()));

            // 查詢分類
            List<Category> categories = getCategories(spu);

            // 查詢組内參數
            List<SpecGroup> specGroups = this.specificationClient.querySpecsByCid(spu.getCid3());

            // 查詢所有特有規格參數
            List<SpecParam> specParams = this.specificationClient.querySpecParam(null, spu.getCid3(), null, false);
            // 處理規格參數
            Map<Long, String> paramMap = new HashMap<>();
            specParams.forEach(param->{
                paramMap.put(param.getId(), param.getName());
            });

            Map<String, Object> map = new HashMap<>();
            map.put("spu", spu);
            map.put("spuDetail", spuDetail);
            map.put("skus", skus);
            map.put("brand", brands.get(0));
            map.put("categories", categories);
            map.put("groups", specGroups);
            map.put("params", paramMap);
            return map;
        } catch (Exception e) {
            logger.error("加載商品資料出錯,spuId:{}", spuId, e);
        }
        return null;
    }

    private List<Category> getCategories(Spu spu) {
        try {
            List<String> names = this.categoryClient.queryNameByIds(
                    Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
            Category c1 = new Category();
            c1.setName(names.get(0));
            c1.setId(spu.getCid1());

            Category c2 = new Category();
            c2.setName(names.get(1));
            c2.setId(spu.getCid2());

            Category c3 = new Category();
            c3.setName(names.get(2));
            c3.setId(spu.getCid3());

            return Arrays.asList(c1, c2, c3);
        } catch (Exception e) {
            logger.error("查詢商品分類出錯,spuId:{}", spu.getId(), e);
        }
        return null;
    }
}      

然後在controller中把資料放入model:

@Controller
@RequestMapping("item")
public class GoodsController {

    @Autowired
    private GoodsService goodsService;
    /**
     * 跳轉到商品詳情頁
     * @param model
     * @param id
     * @return
     */
    @GetMapping("{id}.html")
    public String toItemPage(Model model, @PathVariable("id")Long id){
        // 加載所需的資料
        Map<String, Object> modelMap = this.goodsService.loadModel(id);
        // 放入模型
        model.addAllAttributes(modelMap);
        return "item";
    }
}      

1.4.4.頁面測試資料

我們在頁面中先寫一段JS,把模型中的資料取出觀察,看是否成功:

<script th:inline="javascript">
    const a = /*[[${groups}]]*/ [];
    const b = /*[[${params}]]*/ [];
    const c = /*[[${categories}]]*/ [];
    const d = /*[[${spu}]]*/ {};
    const e = /*[[${spuDetail}]]*/ {};
    const f = /*[[${skus}]]*/ [];
    const g = /*[[${brand}]]*/ {};
</script>      

然後檢視頁面源碼:

商城項目-商品詳情

資料都成功查到了!

1.5.渲染面包屑

在商品展示頁的頂部,有一個商品分類、品牌、标題的面包屑

其資料有3部分:

  • 商品分類
  • 商品品牌
  • spu标題

我們的模型中都有,是以直接渲染即可(頁面101行開始):

<div class="crumb-wrap">
    <ul class="sui-breadcrumb">
        <li th:each="category : ${categories}">
            <a href="#" th:text="${category.name}">手機</a>
        </li>
        <li>
            <a href="#" th:text="${brand.name}">Apple</a>
        </li>
        <li class="active" th:text="${spu.title}">Apple iPhone 6s</li>
    </ul>
</div>      

1.6.渲染商品清單

先看下整體效果:

商城項目-商品詳情

這個部分需要渲染的資料有5塊:

  • sku圖檔
  • sku标題
  • 副标題
  • sku價格
  • 特有規格屬性清單

其中,sku 的圖檔、标題、價格,都必須在使用者選中一個具體sku後,才能渲染。而特有規格屬性清單可以在spuDetail中查詢到。而副标題則是在spu中,直接可以在頁面渲染

是以,我們先對特有規格屬性清單進行渲染。等使用者選擇一個sku,再通過js對其它sku屬性渲染

1.6.1.副标題

副标題是在spu中,是以我們直接通過Thymeleaf渲染:

在第146行左右:

<div class="news"><span th:utext="${spu.subTitle}"></span></div>      

副标題中可能會有超連結,是以這裡也用​

​th:utext​

​來展示,效果:

1.6.2.渲染規格屬性清單

規格屬性清單将來會有事件和動态效果。我們需要有js代碼參與,不能使用Thymeleaf來渲染了。

是以,這裡我們用vue,不過需要先把資料放到js對象中,友善vue使用

初始化資料

我們在頁面的​

​head​

​中,定義一個js标簽,然後在裡面定義變量,儲存與sku相關的一些資料:

<script th:inline="javascript">
    // sku集合
    const skus = /*[[${skus}]]*/ [];
    // 規格參數id與name對
    const paramMap = /*[[${params}]]*/ {};
    // 特有規格參數集合
    const specialSpec = JSON.parse(/*[[${spuDetail.specialSpec}]]*/ "");
</script>      
  • specialSpec:這是SpuDetail中唯一與Sku相關的資料

    是以我們并沒有儲存整個spuDetail,而是隻保留了這個屬性,而且需要手動轉為js對象。

  • paramMap:規格參數的id和name對,友善頁面根據id擷取參數名
  • sku:特有規格參數集合

我們來看下頁面擷取的資料:

商城項目-商品詳情

通過Vue渲染

我們把剛才獲得的幾個變量儲存在Vue執行個體中:

商城項目-商品詳情

然後在頁面中渲染:

<div id="specification" class="summary-wrap clearfix">
    <dl v-for="(v,k) in specialSpec" :key="k">
        <dt>
            <div class="fl title">
                <i>{{paramMap[k]}}</i>
            </div>
        </dt>
        <dd v-for="(str,j) in v" :key="j">
            <a href="javascript:;" class="selected">
                {{str}}<span title="點選取消選擇">&nbsp;</span>
            </a>
        </dd>
    </dl>
</div>      

然後重新整理頁面檢視:

商城項目-商品詳情

資料成功渲染了。不過我們發現所有的規格都被勾選了。這是因為現在,每一個規格都有樣式:​

​selected​

​,我們應該隻選中一個,讓它的class樣式為selected才對!

那麼問題來了,我們該如何确定使用者選擇了哪一個?

1.6.3.規格屬性的篩選

分析

規格參數的格式是這樣的:

商城項目-商品詳情

每一個規格項是數組中的一個元素,是以我們隻要儲存被選擇的規格項的索引,就能判斷哪個是使用者選擇的了!

我們需要一個對象來儲存使用者選擇的索引,格式如下:

{
    "4":0,
    "12":0,
    "13":0
}      

但問題是,第一次進入頁面時,使用者并未選擇任何參數。是以索引應該有一個預設值,我們将預設值設定為0。

我們在​

​head​

​的script标簽中,對索引對象進行初始化:

然後在vue中儲存:

頁面改造

我們在頁面中,通過判斷indexes的值來判斷目前規格是否被選中,并且給規格綁定點選事件,點選規格項後,修改indexes中的對應值:

<div id="specification" class="summary-wrap clearfix">
    <dl v-for="(v,k) in specialSpec" :key="k">
        <dt>
            <div class="fl title">
                <i>{{paramMap[k]}}</i>
            </div>
        </dt>
        <dd v-for="(str,j) in v" :key="j">
            <a href="javascript:;" :class="{selected: j===indexes[k]}" @click="indexes[k]=j">
                {{str}}<span v-if="j===indexes[k]" title="點選取消選擇">&nbsp;</span>
            </a>
        </dd>
    </dl>
</div>      

效果:

商城項目-商品詳情

1.6.4.确定SKU

在我們設計sku資料的時候,就已經添加了一個字段:indexes:

商城項目-商品詳情

這其實就是規格參數的索引組合。

而我們在頁面中,使用者點選選擇規格後,就會把對應的索引儲存起來:

商城項目-商品詳情

是以,我們可以根據這個indexes來确定使用者要選擇的sku

我們在vue中定義一個​計算屬性​,來計算與索引比對的sku:

computed:{
    sku(){
        const index = Object.values(this.indexes).join("_");
        return this.skus.find(s => s.indexes = index);
    }
}      

在浏覽器工具中檢視:

商城項目-商品詳情

1.6.5.渲染sku清單

既然已經拿到了使用者選中的sku,接下來,就可以在頁面渲染資料了

圖檔清單

商品圖檔是一個字元串,以​

​,​

​分割,頁面展示比較麻煩,是以我們編寫一個​計算屬性​:images(),将圖檔字元串變成數組:

computed: {
    sku(){
        const index = Object.values(this.indexes).join("_");
        return this.skus.find(s=>s.indexes==index);
    },
    images(){
        return this.sku.images ? this.sku.images.split(",") : [''];
    }
},      

頁面改造:

<div class="zoom">
   <!--預設第一個預覽-->
   <div id="preview" class="spec-preview">
      <span class="jqzoom">
         <img :jqimg="images[0]" :src="images[0]" width="400px" height="400px"/>
      </span>
   </div>
   <!--下方的縮略圖-->
   <div class="spec-scroll">
      <a class="prev"><</a>
      <!--左右按鈕-->
      <div class="items">
         <ul>
            <li v-for="(image, i) in images" :key="i">
               <img :src="image" :bimg="image" onmousemove="preview(this)" />
            </li>
         </ul>
      </div>
      <a class="next">></a>
   </div>
</div>      

完整效果

商城項目-商品詳情

1.7.商品詳情

分成上下兩部分:

  • 上部:展示的是規格屬性清單
  • 下部:展示的是商品詳情

1.7.1.屬性清單(作業)

這部分内容與規格參數部分重複,我就不帶大家做了,大家可以自己完成

1.7.2.商品詳情

商品詳情是HTML代碼,我們不能使用 ​

​th:text​

​​,應該使用​

​th:utext​

在頁面的第444行左右:

<!--商品詳情-->
<div class="intro-detail" th:utext="${spuDetail.description}">
</div>      

最終展示效果:

商城項目-商品詳情

1.8.規格包裝:

規格包裝分成兩部分:

  • 規格參數
  • 包裝清單

而且規格參數需要按照組來顯示

1.8.1.規格參數

最終的效果:

商城項目-商品詳情

我們模型中有一個groups,跟這個資料結果很像:

商城項目-商品詳情

分成8個組,組内都有params,裡面是所有的參數。不過,這些參數都沒有值!

規格參數的值分為兩部分:

  • 通用規格參數:儲存在SpuDetail中的genericSpec中
  • 特有規格參數:儲存在sku的ownSpec中

我們需要把這兩部分值取出來,放到groups中。

從spuDetail中取出genericSpec并取出groups:

商城項目-商品詳情

把genericSpec引入到Vue執行個體:

商城項目-商品詳情

因為sku是動态的,是以我們編寫一個​計算屬性​,來進行值的組合:

groups(){
    groups.forEach(group => {
        group.params.forEach(param => {
            if(param.generic){
                // 通用屬性,去spu的genericSpec中擷取
                param.v = this.genericSpec[param.id] || '其它';
            }else{
                // 特有屬性值,去SKU中擷取
                param.v = JSON.parse(this.sku.ownSpec)[param.id]
            }
        })
    })
    return groups;
}      

然後在頁面渲染:

<div class="Ptable">
    <div class="Ptable-item" v-for="group in groups" :key="group.name">
        <h3>{{group.name}}</h3>
        <dl>
            <div v-for="p in group.params">
                <dt>{{p.name}}</dt><dd>{{p.v + (p.unit || '')}}</dd>
            </div>
        </dl>
    </div>
</div>      

1.8.2.包裝清單

包裝清單在商品詳情中,我們一開始并沒有指派到Vue執行個體中,但是可以通過Thymeleaf來渲染

<div class="package-list">
    <h3>包裝清單</h3>
    <p th:text="${spuDetail.packingList}"></p>
</div>      

最終效果:

商城項目-商品詳情
商城項目-商品詳情

1.9.售後服務

售後服務也可以通過Thymeleaf進行渲染:

<div id="three" class="tab-pane">
    <p>售後保障</p>
    <p th:text="${spuDetail.afterService}"></p>
</div>      

效果: