专栏首页cwl_Java商城项目-商品新增

商城项目-商品新增

5.商品新增

5.1.效果预览

新增商品窗口:

这个表单比较复杂,因为商品的信息比较多,分成了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,略

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 经典面试题-简述HttpSession 的作用、使用方法,可用代码说明

    HttpSession 中可以跟踪并储存用户信息,把值设置到属性中,有2 个方法:setAttribute(),getAttrribute();

    cwl_java
  • 商城项目-商品规格数据结构

    乐优商城是一个全品类的电商网站,因此商品的种类繁多,每一件商品,其属性又有差别。为了更准确描述商品及细分差别,抽象出两个概念:SPU和SKU,了解一下:

    cwl_java
  • PHP-ThinkPHP将后台模板与框架做结合

    商品列表 Goods控制器index方法 goods_list.html -> index.html

    cwl_java
  • Python报错: No module named 'requests'

    运行.py的文件的时候,有时候会遇到这样的报错。不要慌,如果你掌握了面向百度编程的话,能够解决百分之90的bug,专治各种疑难杂症。

    王小婷
  • Spring Boot 如何部署到 Linux 中的服务

    https://docs.ossez.com/spring-boot-docs/docs/reference/html/deployment.html

    HoneyMoose
  • Spring Boot 如何部署到 Linux 中的服务

    https://docs.ossez.com/spring-boot-docs/docs/reference/html/deployment.html

    HoneyMoose
  • Spring集成Hadoop和Hbase

    hadoop是大数据环境下必备的一套系统,使用hadoop集群可以充分的共享服务器资源,在离线处理上已经有了多年的应用。

    黑白格
  • 第5期 ARTS 打卡计划

    通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以...

    闻人的技术博客
  • 16-GPRS(Air202)关于多个文件中的变量调用和定时器

    大家有没有看这篇 https://www.cnblogs.com/yangfengwu/p/8965054.html

    杨奉武
  • 真实经历:手机浏览器被劫持了怎么办?

    明月作为一名草根博客站长手机端拥有几个测试网站移动端效果的浏览器是很正常的吧,可就是在前两天入手了红米 5 Plus 后(可参考【新入手 红米 5 Plus 手...

    明月云服务

扫码关注云+社区

领取腾讯云代金券