天天看點

商城項目-商品新增5.商品新增

5.商品新增

5.1.效果預覽

新增商品視窗:

商城項目-商品新增5.商品新增

這個表單比較複雜,因為商品的資訊比較多,分成了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.商品新增

5.3.新增商品頁的基本架構

5.3.1.Steppers,步驟線

預覽效果圖中,分四個步驟顯示商品表單的元件,叫做stepper,看下文檔:

商城項目-商品新增5.商品新增

其基本結構如圖:

商城項目-商品新增5.商品新增

一個步驟線(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.商品新增

步驟線出現了!

那麼問題來了:該如何讓這幾個步驟切換呢?

5.3.3.步驟切換按鈕

分析

如果改變step的值與指定的步驟索引一緻,就可以實作步驟切換了:

商城項目-商品新增5.商品新增

是以,我們需要定義兩個按鈕,點選後修改step的值,讓步驟前進或後退。

那麼這兩個按鈕放哪裡?

如果放在MyGoodsForm内,當表單内容過多時,按鈕會被擠壓到螢幕最下方,不夠友好。最好是能夠懸停狀态。

是以,按鈕必須放到MyGoods元件中,也就是父元件。

父元件的對話框是一個card,card元件提供了一個滾動效果,scrollable,如果為true,card的内容滾動時,其頭部和底部是可以靜止的。

現在card的頭部是彈框的标題,card的中間就是表單内容。如果我們把按鈕放到底部,就可以實作懸停效果。

頁面添加按鈕

改造MyGoods的對話框元件:

商城項目-商品新增5.商品新增

檢視頁面:

商城項目-商品新增5.商品新增

添加點選事件

現在這兩個按鈕點選後沒有任何反應。我們需要給他們綁定點選事件,來修改MyGoodsForm中的step的值。

也就是說,父元件要修改子元件的屬性狀态。想到什麼了?

props屬性。

我們先在父元件定義一個step屬性:

商城項目-商品新增5.商品新增

然後在點選事件中修改它:

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.商品新增

測試效果:

商城項目-商品新增5.商品新增

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級分類

效果:

商城項目-商品新增5.商品新增

檢視goods的屬性,三級類目都在:

商城項目-商品新增5.商品新增

5.4.3.品牌選擇

select元件

品牌不分級别,使用普通下拉選框即可。我們檢視官方文檔的下拉選框說明:

商城項目-商品新增5.商品新增

元件名: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需要我們去背景查詢,而且必須是在使用者選擇商品分類後去查詢。

我們定義一個屬性,儲存品牌的待選項資訊:

商城項目-商品新增5.商品新增

然後編寫一個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.商品新增

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.5.1.什麼是富文本編輯器

百度百科:

商城項目-商品新增5.商品新增

通俗來說:富文本,就是比較豐富的文本編輯器。普通的框隻能輸入文字,而富文本還能給文字加顔色樣式等。

富文本編輯器有很多,例如: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.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.自定義的富文本編輯器

不過這個元件有個小問題,就是圖檔上傳的無法直接上傳到背景,是以我們對其進行了封裝,支援了圖檔的上傳。

商城項目-商品新增5.商品新增

使用也非常簡單:

<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.商品新增

5.6.規格參數

商品規格參數與商品分類綁定,是以我們需要在使用者選擇商品分類後,去背景查詢對應的規格參數模闆。

5.6.1.查詢商品規格

首先,我們在data中定義變量,記錄查詢到的規格參數模闆:

商城項目-商品新增5.商品新增

然後,我們通過watch監控goods.categories的變化,然後去查詢規格:

商城項目-商品新增5.商品新增

檢視是否查詢到:

商城項目-商品新增5.商品新增

5.6.2.頁面展示規格屬性

擷取到了規格參數,還需要把它展示到頁面中。

現在查詢到的規格參數隻有key,并沒有值。值需要使用者來根據SPU資訊填寫,是以規格參數最終需要處理為表單。

整體結構

整體來看,規格參數是數組,每個元素是一組規格的集合。我們需要分組來展示。比如每組放到一個card中。

注意事項:

規格參數中的屬性有一些需要我們特殊處理:

商城項目-商品新增5.商品新增
  • 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.商品新增

5.7.SKU特有屬性

sku特有屬性也存在與specifications中,但是我們現在隻想展示特有屬性,而不是從頭周遊一次。是以,我們應該從specifications中把特有規格屬性拆分出來獨立儲存。

5.7.1.篩選特有規格參數

首先:我們在data中建立一個屬性,儲存特有的規格參數:

商城項目-商品新增5.商品新增

然後,在查詢完成規格模闆後,立刻對規格參數進行處理,篩選出特有規格參數,儲存到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.商品新增

5.7.2.頁面渲染SKU屬性

接下來,我們把篩選出的特有規格參數,渲染到SKU頁面:

我們的目标效果是這樣的:

商城項目-商品新增5.商品新增

可以看到,

  • 每一個特有屬性自成一組,都包含标題和選項。我們可以使用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>           

複制

我們的實作效果:

商城項目-商品新增5.商品新增

測試下,勾選checkbox或填寫文本會發生什麼:

商城項目-商品新增5.商品新增

看下規格模闆的值:

商城項目-商品新增5.商品新增

5.7.3.自由添加或删除文本框

剛才的實作中,普通文本項隻有一個,如果使用者想添加更多值就不行。我們需要讓使用者能夠自由添加新的文本框,而且還能删除。

這裡有個取巧的方法:

還記得我們初始化 特有規格參數時,新增了一個selected屬性嗎,用來儲存使用者填寫的值,是一個數組。每當使用者新加一個值,該數組的長度就會加1,而初始長度為0

另外,v-for指令有個特殊之處,就在于它可以周遊數字。比如 v-for=“i in 10”,你會得到1~10

是以,我們可以周遊selected的長度,每當我們輸入1個文本,selected長度會加1,自然會多出一個文本框。

代碼如下:

商城項目-商品新增5.商品新增
<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.商品新增

而删除文本框相對就比較簡單了,隻要在文本框末尾添加一個按鈕,添加點選事件即可,代碼:

商城項目-商品新增5.商品新增

添加了一些布局樣式,以及一個按鈕,在點選事件中删除一個值。

5.8.展示SKU清單

5.8.1.效果預覽

當我們標明SKU的特有屬性時,就會對應出不同排列組合的SKU。

舉例:

商城項目-商品新增5.商品新增

當你選擇了上圖中的這些選項時:

  • 顔色共2種:土豪金,絢麗紅
  • 記憶體共2種:2GB,4GB
  • 機身存儲1種:64GB

此時會産生多少種SKU呢? 應該是 2 * 2 * 1 = 4種。

是以,接下來應該由使用者來對這4種sku的資訊進行詳細填寫,比如庫存和價格等。而多種sku的最佳展示方式,是表格(淘寶、京東都是這麼做的),如圖:

商城項目-商品新增5.商品新增

而且這個表格應該随着使用者選擇的不同而動态變化。如何實作?

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);           

複制

結果:

商城項目-商品新增5.商品新增

完美實作。

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.商品新增

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"},
]           

複制

思路是這樣:

  • 我們的啟點是一個空的對象數組:

    [{}]

  • 然後先與第一個規格求笛卡爾積
  • 然後再把結果與下一個規格求笛卡爾積,依次類推

如果:

商城項目-商品新增5.商品新增

代碼:

我們在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
        }, [{}])
    }
}           

複制

結果:

商城項目-商品新增5.商品新增

優化:這裡生成的是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.商品新增

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,讓使用者選擇,如圖:

商城項目-商品新增5.商品新增

代碼:

<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.商品新增

5.8.5.圖檔上傳清單

這個表格中隻展示了基本資訊,當使用者需要上傳圖檔時,該怎麼做呢?

Vuetify的table有一個展開功能,可以提供額外的展示空間:

商城項目-商品新增5.商品新增

用法也非常簡單,添加一個template,把其slot屬性指定為expand即可:

商城項目-商品新增5.商品新增

效果:

商城項目-商品新增5.商品新增

接下來就是我們的圖檔上傳元件:v-upload

商城項目-商品新增5.商品新增

5.9.表單送出

5.9.1.添加送出按鈕

我們在step=4,也就是SKU屬性清單頁面, 添加一個送出按鈕。

<!--送出按鈕-->
<v-flex xs3 offset-xs9>
    <v-btn color="info">儲存商品資訊</v-btn>
</v-flex>           

複制

效果:

商城項目-商品新增5.商品新增

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)
}           

複制

點選測試,看效果:

商城項目-商品新增5.商品新增

向背景發起請求,因為請求體複雜,我們直接發起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,略