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="點選取消選擇"> </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="點選取消選擇"> </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>
效果: