商城-品牌的新增
1.品牌的新增
昨天我们完成了品牌的查询,接下来就是新增功能。
1.1.页面实现
1.1.1.初步编写弹窗
当我们点击新增按钮,应该出现一个弹窗,然后在弹窗中出现一个表格,我们就可以填写品牌信息了。
我们查看Vuetify官网,弹窗是如何实现:
另外,我们可以通过文档看到对话框的一些属性:
- value:控制窗口的可见性,true可见,false,不可见
- max-width:控制对话框最大宽度
- scrollable :是否可滚动,要配合v-card来使用,默认是false
- persistent :点击弹窗以外的地方不会关闭弹窗,默认是false
现在,我们来使用一下。
首先,我们在data中定义一个show属性,来控制对话框的显示状态:
然后,在页面添加一个v-dialog
<!--弹出的对话框-->
<v-dialog max-width="500" v-model="show" persistent>
<v-card>
<!--对话框的标题-->
<v-toolbar dense dark color="primary">
<v-toolbar-title>新增品牌</v-toolbar-title>
</v-toolbar>
<!--对话框的内容,表单-->
<v-card-text class="px-5">
我是表单
</v-card-text>
</v-card>
</v-dialog>
说明:
-
我们给dialog指定了3个属性,分别是
- max-width:限制宽度
- v-model:value值双向绑定到show变量,用来控制窗口显示
- persisitent:控制窗口不会被意外关闭
-
因为可滚动需要配合
v-card
使用,因此我们在对话框中加入了一个v-card
- 在
v-card
的头部添加了一个v-toolbar
,作为窗口的头部,并且写了标题为:新增品牌- dense:紧凑显示
- dark:黑暗主题
- color:颜色,primary就是整个网站的主色调,蓝色
- 在
v-card
的内容部分,暂时空置,等会写表单
- 在
-
class=“px-5"
:vuetify的内置样式,含义是padding的x轴设置为5,这样表单内容会缩进一些,而不是顶着边框基本语法:
{property}{direction}-{size}
- property:属性,有两种
padding
和margin
p
:对应padding
m
:对应margin
- direction:只padding和margin的作用方向,
t
- 对应margin-top
或者padding-top
属性b
- 对应margin-bottom
orpadding-bottom
l
- 对应margin-left
orpadding-left
r
- 对应margin-right
orpadding-right
x
- 同时对应*-left
和*-right
属性y
- 同时对应*-top
和*-bottom
属性
- size:控制空间大小,基于
$spacer
进行倍增,$spacer
默认是16px0
:将margin
或padding的大小设置为01
- 将margin
或者padding
属性设置为$spacer * .25
2
- 将margin
或者padding
属性设置为$spacer * .5
3
- 将margin
或者padding
属性设置为$spacer
4
- 将margin
或者padding
属性设置为$spacer * 1.5
5
- 将margin
或者padding
属性设置为$spacer * 3
- property:属性,有两种
1.1.2.实现弹窗的可见和关闭
窗口可见
接下来,我们要在点击新增品牌按钮时,将窗口显示,因此要给新增按钮绑定事件。
<v-btn color="primary" @click="addBrand">新增品牌</v-btn>
然后定义一个addBrand方法:
addBrand(){
// 控制弹窗可见:
this.show = true;
}
效果:
窗口关闭
现在,悲剧发生了,因为我们设置了persistent属性,窗口无法被关闭了。除非把show属性设置为false
因此我们需要给窗口添加一个关闭按钮:
<!--对话框的标题-->
<v-toolbar dense dark color="primary">
<v-toolbar-title>新增品牌</v-toolbar-title>
<v-spacer/>
<!--关闭窗口的按钮-->
<v-btn icon @click="closeWindow"><v-icon>close</v-icon></v-btn>
</v-toolbar>
并且,我们还给按钮绑定了点击事件,回调函数为closeWindow。
接下来,编写closeWindow函数:
closeWindow(){
// 关闭窗口
this.show = false;
}
效果:
1.1.3.新增品牌的表单页
接下来就是写表单了。我们有两种选择:
- 直接在dialog对话框中编写表单代码
- 另外编写一个组件,组件内写表单代码。然后在对话框引用组件
选第几种?
我们选第二种方案,优点:
- 表单代码独立组件,可拔插,方便后期的维护。
- 代码分离,可读性更好。
我们新建一个MyBrandForm.vue
组件:
将MyBrandForm引入到MyBrand中,这里使用局部组件的语法:
先导入自定义组件:
// 导入自定义的表单组件
import MyBrandForm from './MyBrandForm'
然后通过components属性来指定局部组件:
components:{
MyBrandForm
}
然后在页面中引用:
页面效果:
1.1.4.编写表单
1.1.4.1.表单
查看文档,找到关于表单的部分:
v-form
,表单组件,内部可以有许多输入项。v-form
有下面的属性:
- value:true,代表表单验证通过;false,代表表单验证失败
v-form
提供了两个方法:
- reset:重置表单数据
- validate:校验整个表单数据,前提是你写好了校验规则。返回Boolean表示校验成功或失败
我们在data中定义一个valid属性,跟表单的value进行双向绑定,观察表单是否通过校验,同时把等会要跟表单关联的品牌brand对象声明出来:
export default {
name: "my-brand-form",
data() {
return {
valid:false, // 表单校验结果标记
brand:{
name:'', // 品牌名称
letter:'', // 品牌首字母
image:'',// 品牌logo
categories:[], // 品牌所属的商品分类数组
}
}
}
}
然后,在页面先写一个表单:
<v-form v-model="valid">
</v-form>
1.1.4.2.文本框
我们的品牌总共需要这些字段:
- 名称
- 首字母
- 商品分类,有很多个
- LOGO
表单项主要包括文本框、密码框、多选框、单选框、文本域、下拉选框、文件上传等。思考下我们的品牌需要哪些?
- 文本框:品牌名称、品牌首字母都属于文本框
- 文件上传:品牌需要图片,这个是文件上传框
- 下拉选框:商品分类提前已经定义好,这里需要通过下拉选框展示,提供给用户选择。
先看文本框,昨天已经用过的,叫做v-text-field
:
查看文档,v-text-field
有以下关键属性:
- append-icon:文本框后追加图标,需要填写图标名称。无默认值
- clearable:是否添加一个清空图标,点击会清空文本框。默认是false
- color:颜色
- counter:是否添加一个文本计数器,在角落显示文本长度,指定true或允许的组大长度。无默认值
- dark:是否应用黑暗色调,默认是false
- disable:是否禁用,默认是false
- flat:是否移除默认的动画效果,默认是false
- full-width:指定宽度为全屏,默认是false
- hide-details:是否因此错误提示,默认是false
- hint:输入框的提示文本
- label:输入框的标签
- multi-line:是否转为文本域,默认是false。文本框和文本域可以自由切换
- placeholder:输入框占位符文本,focus后消失
- required:是否为必填项,如果是,会在label后加*,不具备校验功能。默认是false
- rows:文本域的行数,
multi-line
为true时才有效 - rules:指定校验规则及错误提示信息,数组结构。默认[]
- single-line:是否单行文本显示,默认是false
- suffix:显示后缀
接下来,我们先添加两个字段:品牌名称、品牌的首字母,校验规则暂时不写:
<v-form v-model="valid">
<v-text-field v-model="brand.name" label="请输入品牌名称" required />
<v-text-field v-model="brand.letter" label="请输入品牌首字母" required />
</v-form>
- 千万不要忘了通过
v-model
把表单项与brand
的属性关联起来。
效果:
1.1.4.3.级联下拉选框
接下来就是商品分类了,按照刚才的分析,商品分类应该是下拉选框。
但是大家仔细思考,商品分类包含三级。在展示的时候,应该是先由用户选中1级,才显示2级;选择了2级,才显示3级。形成一个多级分类的三级联动效果。
这个时候,就不是普通的下拉选框,而是三级联动的下拉选框!
这样的选框,在Vuetify中并没有提供(它提供的是基本的下拉框)。因此我已经给大家编写了一个无限级联动的下拉选框,能够满足我们的需求。
具体请参考课前资料的《自定义组件用法指南.md》
我们在代码中使用:
<v-cascader
url="/item/category/list"
multiple
required
v-model="brand.categories"
label="请选择商品分类"/>
- url:加载商品分类选项的接口路径
- multiple:是否多选,这里设置为true,因为一个品牌可能有多个分类
- requried:是否是必须的,这里为true,会在提示上加*,提醒用户
- v-model:关联我们brand对象的categories属性
- label:文字说明
效果:
data中获取的结果:
1.1.4.4.文件上传项
在Vuetify中,也没有文件上传的组件。
还好,我已经给大家写好了一个文件上传的组件:
详细用法,参考《自定义组件使用指南.md》
我们添加上传的组件:
<v-layout row>
<v-flex xs3>
<span style="font-size: 16px; color: #444">品牌LOGO:</span>
</v-flex>
<v-flex>
<v-upload
v-model="brand.image"
url="/upload"
:multiple="false"
:pic-width="250"
:pic-height="90"
/>
</v-flex>
</v-layout>
注意:
- 文件上传组件本身没有提供文字提示。因此我们需要自己添加一段文字说明
- 我们要实现文字和图片组件左右放置,因此这里使用了
v-layout
布局组件:- layout添加了row属性,代表这是一行,如果是column,代表是多行
- layout下面有
v-flex
组件,是这一行的单元,我们有2个单元<v-flex xs3>
:显示文字说明,xs3是响应式布局,代表占12格中的3格- 剩下的部分就是图片上传组件了
v-upload
:图片上传组件,包含以下属性:- v-model:将上传的结果绑定到brand的image属性
- url:上传的路径,我们先随便写一个。
- multiple:是否运行多图片上传,这里是false。因为品牌LOGO只有一个
- pic-width和pic-height:可以控制l图片上传后展示的宽高
最终结果:
1.1.4.5.按钮
上面已经把所有的表单项写完。最后就差提交和清空的按钮了。
在表单的最下面添加两个按钮:
<v-layout class="my-4" row>
<v-spacer/>
<v-btn @click="submit" color="primary">提交</v-btn>
<v-btn @click="clear" >重置</v-btn>
</v-layout>
- 通过layout来进行布局,
my-4
增大上下边距 v-spacer
占用一定空间,将按钮都排挤到页面右侧- 两个按钮分别绑定了submit和clear事件
我们先将方法定义出来:
methods:{
submit(){
// 提交表单
},
clear(){
// 重置表单
}
}
重置表单相对简单,因为v-form组件已经提供了reset方法,用来清空表单数据。只要我们拿到表单组件对象,就可以调用方法了。
我们可以通过$refs
内置对象来获取表单组件。
首先,在表单上定义ref
属性:
然后,在页面查看this.$refs
属性:
看到this.$refs
中只有一个属性,就是myBrandForm
我们在clear中来获取表单对象并调用reset方法:
methods:{
submit(){
// 提交表单
console.log(this);
},
clear(){
// 重置表单
this.$refs.myBrandForm.reset();
// 需要手动清空商品分类
this.categories = [];
}
}
要注意的是,这里我们还手动把this.categories清空了,因为我写的级联选择组件并没有跟表单结合起来。需要手动清空。
1.1.5.表单校验
1.1.5.1.校验规则
Vuetify的表单校验,是通过rules属性来指定的:
校验规则的写法:
说明:
- 规则是一个数组
- 数组中的元素是一个函数,该函数接收表单项的值作为参数,函数返回值两种情况:
- 返回true,代表成功,
- 返回错误提示信息,代表失败
1.1.5.2.项目中代码
我们有四个字段:
- name:做非空校验和长度校验,长度必须大于1
- letter:首字母,校验长度为1,非空。
- image:图片,不做校验,图片可以为空
- categories:非空校验,自定义组件已经帮我们完成,不用写了
首先,我们定义规则:
nameRules:[
v => !!v || "品牌名称不能为空",
v => v.length > 1 || "品牌名称至少2位"
],
letterRules:[
v => !!v || "首字母不能为空",
v => /^[A-Z]{1}$/.test(v) || "品牌字母只能是A~Z的大写字母"
]
然后,在页面标签中指定:
<v-text-field v-model="brand.name" label="请输入品牌名称" required :rules="nameRules" />
<v-text-field v-model="brand.letter" label="请输入品牌首字母" required :rules="letterRules" />
效果:
1.1.6.表单提交
在submit方法中添加表单提交的逻辑:
submit() {
// 1、表单校验
if (this.$refs.myBrandForm.validate()) {
// 2、定义一个请求参数对象,通过解构表达式来获取brand中的属性
const {categories ,letter ,...params} = this.brand;
// 3、数据库中只要保存分类的id即可,因此我们对categories的值进行处理,只保留id,并转为字符串
params.cids = categories.map(c => c.id).join(",");
// 4、将字母都处理为大写
params.letter = letter.toUpperCase();
// 5、将数据提交到后台
this.$http.post('/item/brand', params)
.then(() => {
// 6、弹出提示
this.$message.success("保存成功!");
})
.catch(() => {
this.$message.error("保存失败!");
});
}
}
-
1、通过
this.$refs.myBrandForm
选中表单,然后调用表单的validate
方法,进行表单校验。返回boolean值,true代表校验通过 -
2、通过解构表达式来获取brand中的值,categories和letter需要处理,单独获取。其它的存入params对象中
-
3、品牌和商品分类的中间表只保存两者的id,而brand.categories中保存的数对象数组,里面有id和name属性,因此这里通过数组的map功能转为id数组,然后通过join方法拼接为字符串
-
4、首字母都处理为大写保存
-
5、发起请求
-
6、弹窗提示成功还是失败,这里用到的是我们的自定义组件功能message组件:
这个插件把
$message
对象绑定到了Vue的原型上,因此我们可以通过this.$message
来直接调用。包含以下常用方法:
- info、error、success、warning等,弹出一个带有提示信息的窗口,色调与为普通(灰)、错误(红色)、成功(绿色)和警告(黄色)。使用方法:this.$message.info(“msg”)
- confirm:确认框。用法:
this.$message.confirm("确认框的提示信息")
,返回一个Promise
1.2.后台实现新增
1.2.1.controller
还是一样,先分析四个内容:
- 请求方式:刚才看到了是POST
- 请求路径:/brand
- 请求参数:brand对象,外加商品分类的id数组cids
- 返回值:无
代码:
/**
* 新增品牌
* @param brand
* @return
*/
@PostMapping
public ResponseEntity<Void> saveBrand(Brand brand, @RequestParam("cids") List<Long> cids) {
this.brandService.saveBrand(brand, cids);
return new ResponseEntity<>(HttpStatus.CREATED);
}
1.2.2.Service
这里要注意,我们不仅要新增品牌,还要维护品牌和商品分类的中间表。
@Transactional
public void saveBrand(Brand brand, List<Long> cids) {
// 新增品牌信息
this.brandMapper.insertSelective(brand);
// 新增品牌和分类中间表
for (Long cid : cids) {
this.brandMapper.insertCategoryBrand(cid, brand.getId());
}
}
这里调用了brandMapper中的一个自定义方法,来实现中间表的数据新增
1.2.3.Mapper
通用Mapper只能处理单表,也就是Brand的数据,因此我们手动编写一个方法及sql,实现中间表的新增:
public interface BrandMapper extends Mapper<Brand> {
/**
* 新增商品分类和品牌中间表数据
* @param cid 商品分类id
* @param bid 品牌id
* @return
*/
@Insert("INSERT INTO tb_category_brand (category_id, brand_id) VALUES (#{cid},#{bid})")
int insertCategoryBrand(@Param("cid") Long cid, @Param("bid") Long bid);
}
1.3.请求参数格式错误
1.3.1.原因分析
我们填写表单并提交,发现报错了:
查看控制台的请求详情:
发现请求的数据格式是JSON格式。
原因分析:
axios处理请求体的原则会根据请求数据的格式来定:
-
如果请求体是对象:会转为json发送
-
如果请求体是String:会作为普通表单请求发送,但需要我们自己保证String的格式是键值对。
如:name=jack&age=12
1.3.2.QS工具
QS是一个第三方库,我们可以用npm install qs --save
来安装。不过我们在项目中已经集成了,大家无需安装:
这个工具的名字:QS,即Query String,请求参数字符串。
什么是请求参数字符串?例如: name=jack&age=21
QS工具可以便捷的实现 JS的Object与QueryString的转换。
在我们的项目中,将QS注入到了Vue的原型对象中,我们可以通过this.$qs
来获取这个工具:
我们将this.$qs
对象打印到控制台:
created(){
console.log(this.$qs);
}
发现其中有3个方法:
这里我们要使用的方法是stringify,它可以把Object转为QueryString。
测试一下,使用浏览器工具,把qs对象保存为一个临时变量:
然后调用stringify方法:
成功将person对象变成了 name=jack&age=21的字符串了
1.3.3.解决问题
修改页面,对参数处理后发送:
然后再次发起请求:
发现请求成功:
参数格式:
数据库:
1.4.新增完成后关闭窗口
我们发现有一个问题:新增不管成功还是失败,窗口都一致在这里,不会关闭。
这样很不友好,我们希望如果新增失败,窗口保持;但是新增成功,窗口关闭才对。
因此,我们需要在新增的ajax请求完成以后,关闭窗口
但问题在于,控制窗口是否显示的标记在父组件:MyBrand.vue中。子组件如何才能操作父组件的属性?或者告诉父组件该关闭窗口了?
之前我们讲过一个父子组件的通信,有印象吗?
- 第一步,在父组件中定义一个函数,用来关闭窗口,不过之前已经定义过了,我们优化一下,关闭的同时重新加载数据:
closeWindow(){
// 关闭窗口
this.show = false;
// 重新加载数据
this.getDataFromServer();
}
- 第二步,父组件在使用子组件时,绑定事件,关联到这个函数:
<!--对话框的内容,表单-->
<v-card-text class="px-5">
<my-brand-form @close="closeWindow"/>
</v-card-text>
- 第三步,子组件通过
this.$emit
调用父组件的函数:
测试一下