目录
三、商品新增
此页面单独新建一个组件MyGoodsForm,并且在MyGoods中进行引用:
// 导入自定义的表单组件
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;
//把oldGoods变为null
this.oldGoods = null;
},
3.1 效果预览
这个表单比较复杂,因为商品的信息比较多,分成了4个部分来填写:
-
基本信息
-
商品描述信息
-
规格参数信息
-
SKU信息
3.2 新增商品页基本框架
3.2.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中的对应
3.2.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>
效果:
3.2.3 步骤线切换按钮
分析
方法:通过修改step的值来进行切换。
因此,我们需要定义两个按钮,点击后修改step的值,让步骤前进或后退。
那么这两个按钮放哪里?
如果放在MyGoodsForm内,当表单内容过多时,按钮会被挤压到屏幕最下方,不够友好。最好是能够悬停状态。
所以,按钮必须放到MyGoods组件中,也就是父组件。
父组件的对话框是一个card,card组件提供了一个滚动效果,scrollable,如果为true,card的内容滚动时,其头部和底部是可以静止的。
现在card的头部是弹框的标题,card的中间就是表单内容。如果我们把按钮放到底部,就可以实现悬停效果。
添加按钮
对父组件中的对话框进行改造:
页面效果:
添加点击事件
现在这两个按钮点击后没有任何反应。需要绑定点击事件,来修改MyGoodsForm中的step的值。也就是说,父组件要修改子组件的属性状态。要使用props属性。
先在父组件定义一个step属性:
然后在点击事件中修改它:
在点击下一步时要对所填内容进行有效性验证
页面绑定事件:
然后把step属性传递给子组件:
子组件中接收属性:
最终效果:
3.3 商品基本信息
商品基本信息,主要是一些纯文本比较简单的SPU属性,例如:
商品分类、商品品牌、商品标题、商品卖点(子标题),包装清单,售后服务
接下来,逐步添加这些表单项。
3.3.1 在data中定义goods属性
data() {
return {
goods:{
categories:{}, // 商品3级分类数组信息
brandId: 0,// 品牌id信息
title: '',// 标题
subTitle: '',// 子标题
spuDetail: {
packingList: '',// 包装列表
afterService: '',// 售后服务
},
}
}
注意,这里我们在goods中定义了spuDetail属性,然后把包装列表和售后服务作为它的属性,这样符合数据库的结构。
3.3.2 商品分类选择框
商品分类选框之前已经做过了。是级联选框。直接拿来用:
<v-cascader
url="/item/category/list"
required
showAllLevels
v-model="goods.categories"
label="请选择商品分类"/>
和以前使用有一些区别:
-
一个商品只能有一个分类,所以这里去掉了multiple属性
-
商品SPU中要保存3级商品分类,因此我们这里需要选择showAllLevels属性,显示所有3级分类
效果:
查看goods的属性,三级类目都在:
3.3.3 品牌选择(下拉选择框)
品牌不分级别,使用普通下拉选框即可。查看官方文档的下拉选框说明:
组件名: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需要去后台查询,而且必须是在用户选择商品分类后去查询。
先定义一个属性,保存品牌的待选项信息:
<!--品牌-->
<v-select
:items="brandOptions"
item-text="name"
item-value="id"
label="所属品牌"
v-model="goods.brandId"
required
autocomplete
clearable
dense chips
/>
然后编写一个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
/**
* 根据category的id查询品牌信息
* @param cid
* @return
*/
@GetMapping("cid/{cid}")
public ResponseEntity<List<Brand>> queryBrandByCategoryId(@PathVariable("cid") Long cid){
List<Brand> list = this.brandService.queryBrandByCategoryId(cid);
if (list == null){
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
return ResponseEntity.ok(list);
}
- Mapper
/**
* 根据category id查询brand,左连接
* @param cid
* @return
*/
@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> queryBrandByCategoryId(Long cid);
- Service
接口
/**
* 根据category id查询brand
* @param cid
* @return
*/
List<Brand> queryBrandByCategoryId(Long cid);
实现类
/**
* 根据category id查询brand
* @param cid
* @return
*/
@Override
public List<Brand> queryBrandByCategoryId(Long cid) {
return this.brandMapper.queryBrandByCategoryId(cid);
}
测试
3.3.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:把单行文本变成文本域