5.商品新增
5.1.效果預覽
新增商品視窗:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICMyYTMvw1dvwlMvwlM3VWaWV2Zh1Wa-cmbw5yZvRTO0VWY0YjevwlMzgTM1UjNtUGall3LcVmdhNXLwRHdo9CXt92YucWbpRWdvx2Yx5yazF2Lc9CX6MHc0RHaiojIsJye.png)
這個表單比較複雜,因為商品的資訊比較多,分成了4個部分來填寫:
- 基本資訊
- 商品描述資訊
- 規格參數資訊
- SKU資訊
5.2.從0開始
我們剛剛在查詢時,已經實作建立了MyGoodsForm.vue,并且已經在MyGoods中引入。
不過目前沒有寫代碼:
<template>
<v-card>
my goods form
</v-card>
</template>
<script>
export default {
name: "my-goods-form",
props: {
oldGoods: {
type: Object
},
isEdit: {
type: Boolean,
default: false
}
},
data() {
return {
}
},
methods: {
}
}
</script>
<style scoped>
</style>
複制
然後在MyBrand中,已經引入了MyGoodsForm元件,并且頁面中也形成了對話框:
// 導入自定義的表單元件
import MyGoodsForm from './MyGoodsForm'
複制
<v-dialog max-width="500" v-model="show" persistent>
<v-card>
<!--對話框的标題-->
<v-toolbar dense dark color="primary">
<v-toolbar-title>{{isEdit ? '修改' : '新增'}}商品</v-toolbar-title>
<v-spacer/>
<!--關閉視窗的按鈕-->
<v-btn icon @click="closeWindow">
<v-icon>close</v-icon>
</v-btn>
</v-toolbar>
<!--對話框的内容,表單-->
<v-card-text class="px-5">
<my-goods-form :oldGoods="oldGoods"/>
</v-card-text>
</v-card>
</v-dialog>
複制
并且也已經給新增按鈕綁定了點選事件:
<v-btn color="primary" @click="addGoods">新增商品</v-btn>
複制
addGoods方法中,設定對話框的show屬性為true:
addGoods() {
// 修改标記
this.isEdit = false;
// 控制彈窗可見:
this.show = true;
// 把oldBrand變為null
this.oldBrand = null;
}
複制
不過彈窗中沒有任何資料:
5.3.新增商品頁的基本架構
5.3.1.Steppers,步驟線
預覽效果圖中,分四個步驟顯示商品表單的元件,叫做stepper,看下文檔:
其基本結構如圖:
一個步驟線(v-stepper)總的分為兩部分:
- v-stepper-header:代表步驟的頭部進度條,隻能有一個
- v-stepper-step:代表進度條的每一個步驟,可以有多個
- v-stepper-items:代表目前步驟下的内容組,隻能有一個,内部有stepper-content
- v-stepper-content:代表每一步驟的頁面内容,可以有多個
v-stepper
- value:其值是目前所在的步驟索引,可以用來控制步驟切換
- dark:是否使用黑暗色調,預設false
- non-linear:是否啟用非線性步驟,使用者不用按順序切換,而是可以調到任意步驟,預設false
- vertical:是否垂直顯示步驟線,預設是false,即水準顯示
v-stepper-header的屬性:
- 無
v-stepper-step的屬性
- color:顔色
- complete:目前步驟是否已經完成,布爾值
- editable:是否可編輯任意步驟(非線性步驟)
- step:步驟索引
v-stepper-items
- 無
v-stepper-content
- step:步驟索引,需要與v-stepper-step中的對應
5.3.2.編寫頁面
首先我們在data中定義一個變量,記錄目前的步驟數:
data() {
return {
step: 1, // 目前的步驟數,預設為1
}
},
複制
然後在模闆頁面中引入步驟線:
<v-stepper v-model="step">
<v-stepper-header>
<v-stepper-step :complete="step > 1" step="1">基本資訊</v-stepper-step>
<v-divider/>
<v-stepper-step :complete="step > 2" step="2">商品描述</v-stepper-step>
<v-divider/>
<v-stepper-step :complete="step > 3" step="3">規格參數</v-stepper-step>
<v-divider/>
<v-stepper-step step="4">SKU屬性</v-stepper-step>
</v-stepper-header>
<v-stepper-items>
<v-stepper-content step="1">
基本資訊
</v-stepper-content>
<v-stepper-content step="2">
商品描述
</v-stepper-content>
<v-stepper-content step="3">
規格參數
</v-stepper-content>
<v-stepper-content step="4">
SKU屬性
</v-stepper-content>
</v-stepper-items>
</v-stepper>
複制
效果:
步驟線出現了!
那麼問題來了:該如何讓這幾個步驟切換呢?
5.3.3.步驟切換按鈕
分析
如果改變step的值與指定的步驟索引一緻,就可以實作步驟切換了:
是以,我們需要定義兩個按鈕,點選後修改step的值,讓步驟前進或後退。
那麼這兩個按鈕放哪裡?
如果放在MyGoodsForm内,當表單内容過多時,按鈕會被擠壓到螢幕最下方,不夠友好。最好是能夠懸停狀态。
是以,按鈕必須放到MyGoods元件中,也就是父元件。
父元件的對話框是一個card,card元件提供了一個滾動效果,scrollable,如果為true,card的内容滾動時,其頭部和底部是可以靜止的。
現在card的頭部是彈框的标題,card的中間就是表單内容。如果我們把按鈕放到底部,就可以實作懸停效果。
頁面添加按鈕
改造MyGoods的對話框元件:
檢視頁面:
添加點選事件
現在這兩個按鈕點選後沒有任何反應。我們需要給他們綁定點選事件,來修改MyGoodsForm中的step的值。
也就是說,父元件要修改子元件的屬性狀态。想到什麼了?
props屬性。
我們先在父元件定義一個step屬性:
然後在點選事件中修改它:
previous(){
if(this.step > 1){
this.step--
}
},
next(){
if(this.step < 4){
this.step++
}
}
複制
頁面綁定事件:
<!--底部按鈕,用來操作步驟線-->
<v-card-actions class="elevation-10">
<v-flex class="xs3 mx-auto">
<v-btn @click="previous" color="primary" :disabled="step === 1">上一步</v-btn>
<v-btn @click="next" color="primary" :disabled="step === 4">下一步</v-btn>
</v-flex>
</v-card-actions>
複制
然後把step屬性傳遞給子元件:
<!--對話框的内容,表單-->
<v-card-text class="px-3" style="height: 600px">
<my-goods-form :oldGoods="oldGoods" :step="step"/>
</v-card-text>
複制
子元件中接收屬性:
測試效果:
5.4.商品基本資訊
商品基本資訊,主要是一些純文字比較簡單的SPU屬性,例如:
商品分類、商品品牌、商品标題、商品賣點(子标題),包裝清單,售後服務
接下來,我們一一添加這些表單項。
注:這裡為了簡化,我們就不進行form表單校驗了。之前已經講過。
5.4.1.在data中定義Goods屬性
首先,我們需要定義一個goods對象,包括商品的上述屬性。
data() {
return {
goods:{
categories:{}, // 商品3級分類數組資訊
brandId: 0,// 品牌id資訊
title: '',// 标題
subTitle: '',// 子标題
spuDetail: {
packingList: '',// 包裝清單
afterService: '',// 售後服務
},
}
}
複制
注意,這裡我們在goods中定義了spuDetail屬性,然後把包裝清單和售後服務作為它的屬性,這樣符合資料庫的結構。
5.4.2.商品分類選框
商品分類選框之前我們已經做過了。是級聯選框。直接拿來用:
<v-cascader
url="/item/category/list"
required
showAllLevels
v-model="goods.categories"
label="請選擇商品分類"/>
複制
跟以前使用有一些差別:
- 一個商品隻能有一個分類,是以這裡去掉了multiple屬性
- 商品SPU中要儲存3級商品分類,是以我們這裡需要選擇showAllLevels屬性,顯示所有3級分類
效果:
檢視goods的屬性,三級類目都在:
5.4.3.品牌選擇
select元件
品牌不分級别,使用普通下拉選框即可。我們檢視官方文檔的下拉選框說明:
元件名:v-select
比較重要的一些屬性:
- item-text:選項中用來展示的字段名,預設是text
- item-value:選項中用來作為value值的字段名,預設是value
- items:待選項的對象數組
- label:提示文本
- multiple:是否支援多選,預設是false
其它次要屬性:
- autocomplete:是否根據使用者輸入的文本進行搜尋過濾(自動),預設false
- chips:是否以小紙片方式顯示使用者選中的項,預設false
- clearable:是否添加清空選項圖示,預設是false
- color:顔色
- dense:是否壓縮選擇框高度,預設false
- editable:是否可編輯,預設false
- hide-details:是否隐藏錯誤提示,預設false
- hide-selected:是否在菜單中隐藏已選擇的項
- hint:提示文本
- 其它基本與
元件類似,不再一一列舉v-text-filed
頁面實作
備選項items需要我們去背景查詢,而且必須是在使用者選擇商品分類後去查詢。
我們定義一個屬性,儲存品牌的待選項資訊:
然後編寫一個watch,監控goods.categories的變化:
watch: {
'goods.categories': {
deep: true,
handler(val) {
// 判斷商品分類是否存在,存在才查詢
if (val && val.length > 0) {
// 根據分類查詢品牌
this.$http.get("/item/brand/cid/" + this.goods.categories[2].id)
.then(({data}) => {
this.brandOptions = data;
})
}
}
}
}
複制
我們的品牌對象包含以下字段:id、name、letter、image。顯然item-text應該對應name,item-value應該對應id
是以我們添加一個選框,指定item-text和item-value
<!--品牌-->
<v-select
:items="brandOptions"
item-text="name"
item-value="id"
label="所屬品牌"
v-model="goods.brandId"
required
autocomplete
clearable
dense chips
/>
複制
背景提供接口
頁面需要去背景查詢品牌資訊,我們自然需要提供:
controller
/**
* 根據分類查詢品牌
* @param cid
* @return
*/
@GetMapping("cid/{cid}")
public ResponseEntity<List<Brand>> queryBrandByCategory(@PathVariable("cid") Long cid) {
List<Brand> list = this.brandService.queryBrandByCategory(cid);
if(list == null){
new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return ResponseEntity.ok(list);
}
複制
service
public List<Brand> queryBrandByCategory(Long cid) {
return this.brandMapper.queryByCategoryId(cid);
}
複制
mapper
根據分類查詢品牌有中間表,需要自己編寫Sql:
@Select("SELECT b.* FROM tb_brand b LEFT JOIN tb_category_brand cb ON b.id = cb.brand_id WHERE cb.category_id = #{cid}")
List<Brand> queryByCategoryId(Long cid);
複制
測試效果
5.4.4.标題等其它字段
标題等字段都是普通文本,直接使用
v-text-field
即可:
<v-text-field label="商品标題" v-model="goods.title" :counter="200" required />
<v-text-field label="商品賣點" v-model="goods.subTitle" :counter="200"/>
<v-text-field label="包裝清單" v-model="goods.spuDetail.packingList" :counter="1000" multi-line :rows="3"/>
<v-text-field label="售後服務" v-model="goods.spuDetail.afterService" :counter="1000" multi-line :rows="3"/>
複制
一些新的屬性:
- counter:計數器,記錄目前使用者輸入的文本字數
- rows:文本域的行數
- multi-line:把單行文本變成文本域
5.5.商品描述資訊
商品描述資訊比較複雜,而且圖文并茂,甚至包括視訊。
這樣的内容,一般都會使用富文本編輯器。
5.5.1.什麼是富文本編輯器
百度百科:
通俗來說:富文本,就是比較豐富的文本編輯器。普通的框隻能輸入文字,而富文本還能給文字加顔色樣式等。
富文本編輯器有很多,例如:KindEditor、Ueditor。但并不原生支援vue
但是我們今天要說的,是一款支援Vue的富文本編輯器:
vue-quill-editor
5.5.2.Vue-Quill-Editor
GitHub的首頁:https://github.com/surmon-china/vue-quill-editor
Vue-Quill-Editor是一個基于Quill的富文本編輯器:Quill的官網
5.5.3.使用指南
使用非常簡單:
第一步:安裝,使用npm指令:
npm install vue-quill-editor --save
複制
第二步:加載,在js中引入:
全局使用:
import Vue from 'vue'
import VueQuillEditor from 'vue-quill-editor'
const options = {}; /* { default global options } */
Vue.use(VueQuillEditor, options); // options可選
複制
局部使用:
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import {quillEditor} from 'vue-quill-editor'
var vm = new Vue({
components:{
quillEditor
}
})
複制
第三步:頁面引用:
<quill-editor v-model="goods.spuDetail.description" :options="editorOption"/>
複制
5.5.4.自定義的富文本編輯器
不過這個元件有個小問題,就是圖檔上傳的無法直接上傳到背景,是以我們對其進行了封裝,支援了圖檔的上傳。
使用也非常簡單:
<v-stepper-content step="2">
<v-editor v-model="goods.spuDetail.description" upload-url="/upload/image"/>
</v-stepper-content>
複制
- upload-url:是圖檔上傳的路徑
- v-model:雙向綁定,将富文本編輯器的内容綁定到goods.spuDetail.description
5.5.5.效果:
5.6.規格參數
商品規格參數與商品分類綁定,是以我們需要在使用者選擇商品分類後,去背景查詢對應的規格參數模闆。
5.6.1.查詢商品規格
首先,我們在data中定義變量,記錄查詢到的規格參數模闆:
然後,我們通過watch監控goods.categories的變化,然後去查詢規格:
檢視是否查詢到:
5.6.2.頁面展示規格屬性
擷取到了規格參數,還需要把它展示到頁面中。
現在查詢到的規格參數隻有key,并沒有值。值需要使用者來根據SPU資訊填寫,是以規格參數最終需要處理為表單。
整體結構
整體來看,規格參數是數組,每個元素是一組規格的集合。我們需要分組來展示。比如每組放到一個card中。
注意事項:
規格參數中的屬性有一些需要我們特殊處理:
- global:是否是全局屬性,規格參數中一部分是SPU共享,屬于全局屬性,另一部是SKU特有,需要根據SKU來填寫。是以,在目前版面中,隻展示global為true的,即全局屬性。sku特有屬性放到最後一個面闆
- numerical:是否是數值類型,如果是,把機關補充在頁面表單,不允許使用者填寫,并且要驗證使用者輸入的資料格式
- options:是否有可選項,如果有,則使用下拉選框來渲染。
頁面代碼:
<!--3、規格參數-->
<v-stepper-content step="3">
<v-flex class="xs10 mx-auto px-3">
<!--周遊整個規格參數,擷取每一組-->
<v-card v-for="spec in specifications" :key="spec.group" class="my-2">
<!--組名稱-->
<v-card-title class="subheading">{{spec.group}}</v-card-title>
<!--周遊組中的每個屬性,并判斷是否是全局屬性,不是則不顯示-->
<v-card-text v-for="param in spec.params" :key="param.k" v-if="param.global" class="px-5">
<!--判斷是否有可選項,如果沒有,則顯示文本框。還要判斷是否是數值類型,如果是把unit顯示到字尾-->
<v-text-field v-if="param.options.length <= 0"
:label="param.k" v-model="param.v" :suffix="param.unit || ''"/>
<!--否則,顯示下拉選項-->
<v-select v-else :label="param.k" v-model="param.v" :items="param.options"/>
</v-card-text>
</v-card>
</v-flex>
</v-stepper-content>
複制
效果:
5.7.SKU特有屬性
sku特有屬性也存在與specifications中,但是我們現在隻想展示特有屬性,而不是從頭周遊一次。是以,我們應該從specifications中把特有規格屬性拆分出來獨立儲存。
5.7.1.篩選特有規格參數
首先:我們在data中建立一個屬性,儲存特有的規格參數:
然後,在查詢完成規格模闆後,立刻對規格參數進行處理,篩選出特有規格參數,儲存到specialSpecs中:
// 根據分類查詢規格參數
this.$http.get("/item/spec/" + this.goods.categories[2].id)
.then(({data}) => {
// 儲存全部規格
this.specifications = data;
// 對特有規格進行篩選
const temp = [];
data.forEach(({params}) => {
params.forEach(({k, options, global}) => {
if (!global) {
temp.push({
k, options,selected:[]
})
}
})
})
this.specialSpecs = temp;
})
複制
要注意:我們添加了一個selected屬性,用于儲存使用者填寫的資訊
檢視資料:
5.7.2.頁面渲染SKU屬性
接下來,我們把篩選出的特有規格參數,渲染到SKU頁面:
我們的目标效果是這樣的:
可以看到,
- 每一個特有屬性自成一組,都包含标題和選項。我們可以使用card達到這個效果。
- 無options選項的特有屬性,展示一個文本框,有options選項的,展示多個checkbox,讓使用者選擇
頁面代碼實作:
<!--4、SKU屬性-->
<v-stepper-content step="4">
<v-flex class="mx-auto">
<!--周遊特有規格參數-->
<v-card flat v-for="spec in specialSpecs" :key="spec.k">
<!--特有參數的标題-->
<v-card-title class="subheading">{{spec.k}}:</v-card-title>
<!--特有參數的待選項,需要判斷是否有options,如果沒有,展示文本框,讓使用者自己輸入-->
<v-card-text v-if="spec.options.length <= 0" class="px-5">
<v-text-field :label="'輸入新的' + spec.k" v-model="spec.selected"/>
</v-card-text>
<!--如果有options,需要展示成多個checkbox-->
<v-card-text v-else class="container fluid grid-list-xs">
<v-layout row wrap class="px-5">
<v-checkbox color="primary" v-for="o in spec.options" :key="o" class="flex xs3"
:label="o" v-model="spec.selected" :value="o"/>
</v-layout>
</v-card-text>
</v-card>
</v-flex>
</v-stepper-content>
複制
我們的實作效果:
測試下,勾選checkbox或填寫文本會發生什麼:
看下規格模闆的值:
5.7.3.自由添加或删除文本框
剛才的實作中,普通文本項隻有一個,如果使用者想添加更多值就不行。我們需要讓使用者能夠自由添加新的文本框,而且還能删除。
這裡有個取巧的方法:
還記得我們初始化 特有規格參數時,新增了一個selected屬性嗎,用來儲存使用者填寫的值,是一個數組。每當使用者新加一個值,該數組的長度就會加1,而初始長度為0
另外,v-for指令有個特殊之處,就在于它可以周遊數字。比如 v-for=“i in 10”,你會得到1~10
是以,我們可以周遊selected的長度,每當我們輸入1個文本,selected長度會加1,自然會多出一個文本框。
代碼如下:
<v-card flat v-for="spec in specialSpecs" :key="spec.k">
<!--特有參數的标題-->
<v-card-title class="subheading">{{spec.k}}:</v-card-title>
<!--特有參數的待選項,需要判斷是否有options,如果沒有,展示文本框,讓使用者自己輸入-->
<v-card-text v-if="spec.options.length <= 0" class="px-5">
<div v-for="i in spec.selected.length+1" :key="i">
<v-text-field :label="'輸入新的' + spec.k" v-model="spec.selected[i-1]" v-bind:value="i"/>
</div>
</v-card-text>
<!--如果有options,需要展示成多個checkbox-->
<v-card-text v-else class="container fluid grid-list-xs">
<v-layout row wrap class="px-5">
<v-checkbox color="primary" v-for="o in spec.options" :key="o" class="flex xs3"
:label="o" v-model="spec.selected" :value="o"/>
</v-layout>
</v-card-text>
</v-card>
複制
效果:
而删除文本框相對就比較簡單了,隻要在文本框末尾添加一個按鈕,添加點選事件即可,代碼:
添加了一些布局樣式,以及一個按鈕,在點選事件中删除一個值。
5.8.展示SKU清單
5.8.1.效果預覽
當我們標明SKU的特有屬性時,就會對應出不同排列組合的SKU。
舉例:
當你選擇了上圖中的這些選項時:
- 顔色共2種:土豪金,絢麗紅
- 記憶體共2種:2GB,4GB
- 機身存儲1種:64GB
此時會産生多少種SKU呢? 應該是 2 * 2 * 1 = 4種。
是以,接下來應該由使用者來對這4種sku的資訊進行詳細填寫,比如庫存和價格等。而多種sku的最佳展示方式,是表格(淘寶、京東都是這麼做的),如圖:
而且這個表格應該随着使用者選擇的不同而動态變化。如何實作?
5.8.2.算法:求數組笛卡爾積
大家看這個結果就能發現,這其實是在求多個數組的笛卡爾積。作為一個程式員,這應該是基本功了吧。
兩個數組笛卡爾積
假如有兩個數組,求笛卡爾積,其基本思路是這樣的:
- 在周遊一個數組的同時,周遊另一個數組,然後把元素拼接,放到新數組。
示例1:
const arr1 = ['1','2','3'];
const arr2 = ['a','b','c'];
const result = [];
arr1.forEach(e1 => {
arr2.forEach(e2 => {
result.push(e1 + "_" + e2)
})
})
console.log(result);
複制
結果:
完美實作。
N個數組的笛卡爾積
如果是N個數組怎麼辦?
不确定數組數量,代碼沒有辦法寫死。該如何處理?
思路:
- 先拿其中兩個數組求笛卡爾積
- 然後把前面運算的結果作為新數組,與第三個數組求笛卡爾積
把前兩次運算的結果作為第三次運算的參數。大家想到什麼了?
沒錯,之前講過的一個數組功能:Reduce
reduce函數的聲明:
reduce(callback,initvalue)
複制
callback:是一個回調函數。這個callback可以接收2個參數:arg1,arg2
- arg1代表的上次運算得到的結果
- arg2是數組中正要處理的元素
initvalue,初始化值。第一次調用callback時把initvalue作為第一個參數,把數組的第一個元素作為第二個參數運算。如果未指定,則第一次運算會把數組的前兩個元素作為參數。
reduce會把數組中的元素逐個用這個函數處理,然後把結果作為下一次回調函數的第一個參數,數組下個元素作為第二個參數,以此類推。
是以,我們可以把想要求笛卡爾積的多個數組先放到一個大數組中。形成二維數組。然後再來運算:
示例2:
const arr1 = ['1', '2', '3'];
const arr2 = ['a', 'b'];
// 用來作為運算的二維數組
const arr3 = [arr1, arr2, ['x', 'y']]
const result = arr3.reduce((last, el) => {
const arr = [];
// last:上次運算結果
// el:數組中的目前元素
last.forEach(e1 => {
el.forEach(e2 => {
arr.push(e1 + "_" + e2)
})
})
return arr
});
console.log(result);
複制
結果:
5.8.3.算法結合業務
來看我們的業務邏輯:
首先,我們已經有了一個特有參數的規格模闆:
[
{
"k": "機身顔色",
"selected": ["紅色","黑色"]
},
{
"k": "記憶體",
"selected": ["8GB","6GB"]
},
{
"k": "機身存儲",
"selected": ["64GB","256GB"]
}
]
複制
可以看做是一個二維數組。
一維是參數對象。
二維是參數中的selected選項。
我們想要的結果:
[
{"機身顔色":"紅色","記憶體":"6GB","機身存儲":"64GB"},
{"機身顔色":"紅色","記憶體":"6GB","機身存儲":"256GB"},
{"機身顔色":"紅色","記憶體":"8GB","機身存儲":"64GB"},
{"機身顔色":"紅色","記憶體":"8GB","機身存儲":"256GB"},
{"機身顔色":"黑色","記憶體":"6GB","機身存儲":"64GB"},
{"機身顔色":"黑色","記憶體":"6GB","機身存儲":"256GB"},
{"機身顔色":"黑色","記憶體":"8GB","機身存儲":"64GB"},
{"機身顔色":"黑色","記憶體":"8GB","機身存儲":"256GB"},
]
複制
思路是這樣:
- 我們的啟點是一個空的對象數組:
,[{}]
- 然後先與第一個規格求笛卡爾積
- 然後再把結果與下一個規格求笛卡爾積,依次類推
如果:
代碼:
我們在Vue中新增一個計算屬性,按照上面所講的邏輯,計算所有規格參數的笛卡爾積
computed: {
skus() {
// 過濾掉使用者沒有填寫資料的規格參數
const arr = this.specialSpecs.filter(s => s.selected.length > 0);
// 通過reduce進行累加笛卡爾積
return arr.reduce((last, spec) => {
const result = [];
last.forEach(o => {
spec.selected.forEach(option => {
const obj = {};
Object.assign(obj, o);
obj[spec.k] = option;
result.push(obj);
})
})
return result
}, [{}])
}
}
複制
結果:
優化:這裡生成的是SKU的數組。是以隻包含SKU的規格參數是不夠的。結合資料庫知道,還需要有下面的字段:
- price:價格
- stock:庫存
- enable:是否啟用。雖然笛卡爾積對應了9個SKU,但使用者不一定會需要所有的組合,用這個字段進行标記。
- images:商品的圖檔
- indexes:特有屬性的索引拼接得到的字元串
我們需要給生成的每個sku對象添加上述字段,代碼修改如下:
computed:{
skus(){
// 過濾掉使用者沒有填寫資料的規格參數
const arr = this.specialSpecs.filter(s => s.selected.length > 0);
// 通過reduce進行累加笛卡爾積
return arr.reduce((last, spec, index) => {
const result = [];
last.forEach(o => {
for(let i = 0; i < spec.selected.length; i++){
const option = spec.selected[i];
const obj = {};
Object.assign(obj, o);
obj[spec.k] = option;
// 拼接目前這個特有屬性的索引
obj.indexes = (o.indexes||'') + '_'+ i
if(index === arr.length - 1){
// 如果發現是最後一組,則添加價格、庫存等字段
Object.assign(obj, { price:0, stock:0,enable:false, images:[]})
// 去掉索引字元串開頭的下劃線
obj.indexes = obj.indexes.substring(1);
}
result.push(obj);
}
})
return result
},[{}])
}
}
複制
檢視生成的資料:
5.8.4.頁面展現
頁面展現是一個表格。我們之前已經用過。表格需要以下資訊:
- items:表格内的資料
- headers:表頭資訊
剛才我們的計算屬性skus得到的就是表格資料了。我們還差頭:headers
頭部資訊也是動态的,使用者選擇了一個屬性,就會多出一個表頭。與skus是關聯的。
既然如此,我們再次編寫一個計算屬性,來計算得出header數組:
headers(){
if(this.skus.length <= 0){
return []
}
const headers = [];
// 擷取skus中的任意一個,擷取key,然後周遊其屬性
Object.keys(this.skus[0]).forEach(k => {
let value = k;
if(k === 'price'){
// enable,表頭要翻譯成“價格”
k = '價格'
}else if(k === 'stock'){
// enable,表頭要翻譯成“庫存”
k = '庫存';
}else if(k === 'enable'){
// enable,表頭要翻譯成“是否啟用”
k = '是否啟用'
} else if(k === 'indexes' || k === 'images'){
// 圖檔和索引不在表格中展示
return;
}
headers.push({
text: k,
align: 'center',
sortable: false,
value
})
})
return headers;
}
複制
接下來編寫頁面,實作table。
需要注意的是,price、stock字段需要使用者填寫數值,不能直接展示。enable要展示為checkbox,讓使用者選擇,如圖:
代碼:
<v-card>
<!--标題-->
<v-card-title class="subheading">SKU清單</v-card-title>
<!--SKU表格,hide-actions是以分頁等工具條-->
<v-data-table :items="skus" :headers="headers" hide-actions item-key="indexes">
<template slot="items" slot-scope="props">
<!--價格和庫存展示為文本框-->
<td v-for="(v,k) in props.item" :key="k" v-if="['price', 'stock'].includes(k)"
class="text-xs-center">
<v-text-field single-line v-model.number="props.item[k]"/>
</td>
<!--enable展示為checkbox-->
<td class="text-xs-center" v-else-if="k === 'enable'">
<v-checkbox v-model="props.item[k]"/>
</td>
<!--indexes和images不展示,其它展示為普通文本-->
<td class="text-xs-center" v-else-if="!['indexes','images'].includes(k)">{{v}}</td>
</template>
</v-data-table>
</v-card>
複制
效果:
5.8.5.圖檔上傳清單
這個表格中隻展示了基本資訊,當使用者需要上傳圖檔時,該怎麼做呢?
Vuetify的table有一個展開功能,可以提供額外的展示空間:
用法也非常簡單,添加一個template,把其slot屬性指定為expand即可:
效果:
接下來就是我們的圖檔上傳元件:v-upload
5.9.表單送出
5.9.1.添加送出按鈕
我們在step=4,也就是SKU屬性清單頁面, 添加一個送出按鈕。
<!--送出按鈕-->
<v-flex xs3 offset-xs9>
<v-btn color="info">儲存商品資訊</v-btn>
</v-flex>
複制
效果:
5.9.2點選事件
當使用者點選儲存,我們就需要對頁面的資料進行整理,然後送出到背景服務。
現在我們頁面包含了哪些資訊呢?我們與資料庫對比,看看少什麼
- goods:裡面包含了SPU的幾乎所有資訊
- title:标題
- subtitle:子标題,賣點
- categories:分類對象數組,需要進行整理 **
- brandId:品牌id
- spuDetail:商品詳情
- packingList:包裝清單
- afterService:售後服務
- description:商品描述
- 缺少全局規格屬性specifications **
- 缺少特有規格屬性模闆spec_template **
- skus:包含了sku清單的幾乎所有資訊
- price:價格,需要處理為以分為機關
- stock:庫存
- enable:是否啟用
- indexes:索引
- images:圖檔,數組,需要處理為字元串**
- 缺少其它特有規格,ows_spec **
- 缺少标題:需要根據spu的标題結合特有屬性生成 **
- specifications:全局規格參數的鍵值對資訊
- specialSpec:特有規格參數資訊
在頁面綁定點選事件:
<!--送出按鈕-->
<v-flex xs3 offset-xs9>
<v-btn color="info" @click="submit">儲存商品資訊</v-btn>
</v-flex>
複制
編寫代碼,整理資料:
submit(){
// 表單校驗。 略
// 先處理goods,用結構表達式接收,除了categories外,都接收到goodsParams中
const {categories: [{id:cid1},{id:cid2},{id:cid3}], ...goodsParams} = this.goods;
// 處理規格參數
const specs = this.specifications.map(({group,params}) => {
const newParams = params.map(({options,...rest}) => {
return rest;
})
return {group,params:newParams};
});
// 處理特有規格參數模闆
const specTemplate = {};
this.specialSpecs.forEach(({k, selected}) => {
specTemplate[k] = selected;
});
// 處理sku
const skus = this.skus.filter(s => s.enable).map(({price,stock,enable,images,indexes, ...rest}) => {
// 标題,在spu的title基礎上,拼接特有規格屬性值
const title = goodsParams.title + " " + Object.values(rest).join(" ");
return {
price: this.$format(price+""),stock,enable,indexes,title,// 基本屬性
images: !images ? '' : images.join(","), // 圖檔
ownSpec: JSON.stringify(rest), // 特有規格參數
}
});
Object.assign(goodsParams, {
cid1,cid2,cid3, // 商品分類
skus, // sku清單
})
goodsParams.spuDetail.specifications= JSON.stringify(specs);
goodsParams.spuDetail.specTemplate = JSON.stringify(specTemplate);
console.log(goodsParams)
}
複制
點選測試,看效果:
向背景發起請求,因為請求體複雜,我們直接發起Json請求:
this.$http.post("/item/goods",goodsParams)
.then(() => {
// 成功,關閉視窗
this.$emit('close');
// 提示成功
this.$message.success("新增成功了")
})
.catch(() => {
this.$message.error("儲存失敗!");
});
})
複制
5.9.3.背景編寫接口
實體類
Spu
@Table(name = "tb_spu")
public class Spu {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long brandId;
private Long cid1;// 1級類目
private Long cid2;// 2級類目
private Long cid3;// 3級類目
private String title;// 标題
private String subTitle;// 子标題
private Boolean saleable;// 是否上架
private Boolean valid;// 是否有效,邏輯删除用
private Date createTime;// 建立時間
private Date lastUpdateTime;// 最後修改時間
}
複制
SpuDetail
@Table(name="tb_spu_detail")
public class SpuDetail {
@Id
private Long spuId;// 對應的SPU的id
private String description;// 商品描述
private String specTemplate;// 商品特殊規格的名稱及可選值模闆
private String specifications;// 商品的全局規格屬性
private String packingList;// 包裝清單
private String afterService;// 售後服務
}
複制
Sku
@Table(name = "tb_sku")
public class Sku {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long spuId;
private String title;
private String images;
private Long price;
private String ownSpec;// 商品特殊規格的鍵值對
private String indexes;// 商品特殊規格的下标
private Boolean enable;// 是否有效,邏輯删除用
private Date createTime;// 建立時間
private Date lastUpdateTime;// 最後修改時間
@Transient
private Long stock;// 庫存
}
複制
注意:這裡儲存了一個庫存字段,在資料庫中是另外一張表儲存的,友善查詢。
Stock
@Table(name = "tb_stock")
public class Stock {
@Id
private Long skuId;
private Integer seckillStock;// 秒殺可用庫存
private Integer seckillTotal;// 已秒殺數量
private Integer stock;// 正常庫存
}
複制
Controller
四個問題:
- 請求方式:POST
- 請求路徑:/goods
-
請求參數:Spu的json格式的對象,spu中包含spuDetail和Sku集合。這裡我們該怎麼接收?我們之前定義了一個SpuBo對象,作為業務對象。這裡也可以用它,不過需要再擴充spuDetail和skus字段:
public class SpuBo extends Spu { @Transient String cname;// 商品分類名稱 @Transient String bname;// 品牌名稱 @Transient SpuDetail spuDetail;// 商品詳情 @Transient List<Sku> skus;// sku清單 }
- 傳回類型:無
代碼:
/**
* 新增商品
* @param spu
* @return
*/
@PostMapping
public ResponseEntity<Void> saveGoods(@RequestBody Spu spu) {
try {
this.goodsService.save(spu);
return new ResponseEntity<>(HttpStatus.CREATED);
} catch (Exception e) {
e.printStackTrace();
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
複制
注意:通過@RequestBody注解來接收Json請求
Service
這裡的邏輯比較複雜,我們除了要對SPU新增以外,還要對SpuDetail、Sku、Stock進行儲存
@Transactional
public void save(SpuBo spu) {
// 儲存spu
spu.setSaleable(true);
spu.setValid(true);
spu.setCreateTime(new Date());
spu.setLastUpdateTime(spu.getCreateTime());
this.spuMapper.insert(spu);
// 儲存spu詳情
spu.getSpuDetail().setSpuId(spu.getId());
this.spuDetailMapper.insert(spu.getSpuDetail());
// 儲存sku和庫存資訊
saveSkuAndStock(spu.getSkus(), spu.getId());
}
private void saveSkuAndStock(List<Sku> skus, Long spuId) {
for (Sku sku : skus) {
if (!sku.getEnable()) {
continue;
}
// 儲存sku
sku.setSpuId(spuId);
// 預設不參與任何促銷
sku.setCreateTime(new Date());
sku.setLastUpdateTime(sku.getCreateTime());
this.skuMapper.insert(sku);
// 儲存庫存資訊
Stock stock = new Stock();
stock.setSkuId(sku.getId());
stock.setStock(sku.getStock());
this.stockMapper.insert(stock);
}
}
複制
Mapper
都是通用Mapper,略