1、 商品分类完成以后,自然轮到了品牌功能了
(1)为了方便看到效果,我们新建一个MyBrand.vue,从0开始搭建
(2)修改item的路径
route("/item/brand",'/item/MyBrand',"Brand"),
(3)进入Vuetify的官网:https://vuetifyjs.com/zh-Hans/components/data-tables/
找到服务器端分页和排序
直接复制源代码
(4)完善其对应的源代码
定义与上述冒号对应的方法和数据
继续参考官网的源代码
- 编写相关头信息的代码( :headers=“headers”)
<template>
<div>
<v-data-table
:headers="headers"
:items="desserts"
:options.sync="options"
:server-items-length="totalDesserts"
:loading="loading"
class="elevation-1"
></v-data-table>
</div>
</template>
<script>
export default {
name: "MyBrand",
data(){
return{
headers:[
{ text : "品牌ID",
value : "id",
align:'center',
sortable : true,
},
{ text : "品牌名称",
value : "name",
align:'center',
sortable : false, //设置名称不可排序
},
{ text : "品牌LOGO",
value : "image",
align:'center',
sortable : false, //设置品牌LOGO不可排序
},
{ text : "品牌首字母",
value : "letter",
align:'center',
sortable : true,
}
]
}
}
}
</script>
<style scoped>
</style>
http://manage.leyou.com/#/item/brand
- 完善内容数据相关源代码( :items=“desserts”)这里需要去远程加载数据
<template>
<div>
<v-data-table
:headers="headers"
:items="brands"
:pagination.sync="pagination"
:total-items="totalDesserts"
:loading="loading"
class="elevation-1"
></v-data-table>
</div>
</template>
<script>
export default {
name: "MyBrand",
data(){
return{
headers:[
{ text : "品牌ID",
value : "id",
align:'center',
sortable : true,
},
{ text : "品牌名称",
value : "name",
align:'center',
sortable : false, //设置名称不可排序
},
{ text : "品牌LOGO",
value : "image",
align:'center',
sortable : false, //设置品牌LOGO不可排序
},
{ text : "品牌首字母",
value : "letter",
align:'center',
sortable : true,
}
],
brands:[],
}
},
created() {//编写构造函数
this.brands = [
//占时先编数据,后期从后台加载
{id:1, name: "OPPO",image : "1.jpg", letter:"X" },
{id:262626, name: "飞利浦",image : "11.jpg", letter:"F" },
{id:788, name: "华为",image : "21.jpg", letter:"H" },
{id:2122, name: "小米",image : "31.jpg", letter:"X" },
{id:3139, name: "魅族",image : "41.jpg", letter:"M" },
];
}
}
</script>
<style scoped>
</style>
- 完善其他相关内容
- :loading="loading"设置加载前开启加载后关闭
先默认设置为不显示
- 完善渲染表格的内容
全部代码
<template>
<div>
<v-data-table
:headers="headers"
:items="brands"
:pagination.sync="pagination"
:total-items="totalBrands"
:loading="loading"
class="elevation-1"
>
<template slot="items" slot-scope="props" >
<td class="text-xs-right">{{ props.item.id }}</td>
<td class="text-xs-right">{{ props.item.name }}</td>
<td class="text-xs-right">{{ props.item.image }}</td>
<td class="text-xs-right">{{ props.item.letter }}</td>
</template>
</v-data-table>
</div>
</template>
<script>
export default {
name: "MyBrand",
data(){
return{
headers:[
{ text : "品牌ID",
value : "id",
align:'center',
sortable : true,
},
{ text : "品牌名称",
value : "name",
align:'center',
sortable : false, //设置名称不可排序
},
{ text : "品牌LOGO",
value : "image",
align:'center',
sortable : false, //设置品牌LOGO不可排序
},
{ text : "品牌首字母",
value : "letter",
align:'center',
sortable : true,
}
],
brands:[],
pagination:{},
totalBrands:0,
loading:false,
}
},
created() {//编写构造函数
this.brands = [
//占时先编数据,后期从后台加载
{id:1, name: "OPPO",image : "1.jpg", letter:"X" },
{id:262626, name: "飞利浦",image : "11.jpg", letter:"F" },
{id:788, name: "华为",image : "21.jpg", letter:"H" },
{id:2122, name: "小米",image : "31.jpg", letter:"X" },
{id:3139, name: "魅族",image : "41.jpg", letter:"M" },
];
this.totalBrands = 15;
}
}
</script>
<style scoped>
</style>
- 页面效果
- 设置数据居中
- 设置图片显示
<td class="text-xs-center">
<img :src="props.item.image"/>
</td>
品牌中有,id,name,image,letter字段
(5)完善品牌管理的,新增品牌,搜索框,编辑以及删除按钮
- 设置操作(设置当中的修改和删除按钮)
在headers当中
访问页面效果
完善按钮效果
设置标签缩小 在后面填写small
设置按钮为图标
<td class="text-xs-center">
<v-btn flat icon color="info">
<v-icon>edit</v-icon>
</v-btn>
<v-btn flat icon color="error">
<v-icon>delete</v-icon>
</v-btn>
</td>
效果
- 新增按钮
<v-btn color="info" small>新增品牌</v-btn>
效果
- 搜索框(并控制其宽度)
<v-layout>
<v-flex xs2>
<v-btn color="info" small>新增品牌</v-btn>
</v-flex>
<v-flex xs4>
<v-text-field label="搜索" > </v-text-field>
</v-flex>
</v-layout>
效果
发现页面下方以及按钮不太好看
在搜索框下默认有错误提示
设置搜索框隐藏细节并去掉按钮的small
效果
设置搜索框距离按钮远一些
页面效果
设置搜索框的图标
(6)实现数据的动态查询以及axios
a、发送Ajax请求(异步查询工具)
异步查询数据,自然是通过ajax查询,大家首先想起的肯定是jQuery。但jQuery与MWVM的思想不吻合,而且ajax只是jQuery的一小部分。因此不可能为了发起ajax请求而去引用这么大的一个库。
http://www.axios-js.com/zh-cn/docs/
axios快速入门
执行 GET
请求
// 为给定 ID 的 user 创建请求
axios.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
// 上面的请求也可以这样做
axios.get('/user', {
params: {
ID: 12345
}
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
执行 POST
请求
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
执行多个并发请求
function getUserAccount() {
return axios.get('/user/12345');
}
function getUserPermissions() {
return axios.get('/user/12345/permissions');
}
axios.all([getUserAccount(), getUserPermissions()])
.then(axios.spread(function (acct, perms) {
// 两个请求现在都执行完成
}));
axios API
可以通过向 axios
传递相关配置来创建请求
axios(config)
// 发送 POST 请求
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
});
// 获取远端图片
axios({
method:'get',
url:'http://bit.ly/2mTM3nY',
responseType:'stream'
})
.then(function(response) {
response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
});
b、完善页面的Ajax请求
在http.js当已经将axios对象添加到Vue的静态常量当中,直接可以通过this.$http来调用axios
虽然Vue类以及写好了但是依旧可以往里面添加东西
设置请求测试
this.$http.get("/brand/page",{//设置请求路径
params:{//设置请求参数
page : 1,
}
}).then(resp => {//在http.js当已经将axios对象添加到Vue的静态常量当中,直接可以通过this.$http来调用axios
})
- 搜索功能
双向绑定数据(v-model)
设置初始值
实现发送请求返回对应的数据(设置搜索框改变数据的时候边改变边发送数据)
全部代码
<template>
<div>
<v-layout class="px-4 pb-2">
<v-flex xs2>
<v-btn color="info" >新增品牌</v-btn>
</v-flex>
<v-spacer />
<v-flex xs4>
<v-text-field label="搜索" hide-details append-icon="search" v-model="key"> </v-text-field>
</v-flex>
</v-layout>
<v-data-table
:headers="headers"
:items="brands"
:pagination.sync="pagination"
:total-items="totalBrands"
:loading="loading"
class="elevation-1"
>
<template slot="items" slot-scope="props" >
<td class="text-xs-center">{{ props.item.id }}</td>
<td class="text-xs-center">{{ props.item.name }}</td>
<td class="text-xs-center">
<img :src="props.item.image"/>
</td>
<td class="text-xs-center">{{ props.item.letter }}</td>
<td class="text-xs-center">
<v-btn flat icon color="info">
<v-icon>edit</v-icon>
</v-btn>
<v-btn flat icon color="error">
<v-icon>delete</v-icon>
</v-btn>
</td>
</template>
</v-data-table>
</div>
</template>
<script>
export default {
name: "MyBrand",
data(){
return{
headers:[
{ text : "品牌ID",
value : "id",
align:'center',
sortable : true,
},
{ text : "品牌名称",
value : "name",
align:'center',
sortable : false, //设置名称不可排序
},
{ text : "品牌LOGO",
value : "image",
align:'center',
sortable : false, //设置品牌LOGO不可排序
},
{ text : "品牌首字母",
value : "letter",
align:'center',
sortable : true,
},
{ text : "操作",
align:'center',
sortable : false,
}
],
brands:[],
pagination:{},
totalBrands:0,
loading:false, //设置初始不显示加载数据的动画
key:"", //搜索条件
}
},
created() {//编写构造函数
this.brands = [
//占时先编数据,后期从后台加载
{id:1, name: "OPPO",image : "1.jpg", letter:"X" },
{id:262626, name: "飞利浦",image : "11.jpg", letter:"F" },
{id:788, name: "华为",image : "21.jpg", letter:"H" },
{id:2122, name: "小米",image : "31.jpg", letter:"X" },
{id:3139, name: "魅族",image : "41.jpg", letter:"M" },
];
this.totalBrands = 15;
},
// 去后台查询
watch:{//设置监控,一旦key的值发送改变就发送请求从新加载数据
key(){
this.loadBrands();
}
},
methods:{
loadBrands(){
this.$http.get("/brand/page",{
params:{
//搜索条件
key: this.key //搜索条件后面的key是和v-model绑定的数据
}
})
}
}
}
</script>
<style scoped>
</style>
运行效果
- 分页查询(设置pagination)
Vuetify在this的pagination当中提供了默认的分页信息
每当中分页信息发生变化的时候就方式请求,所以需要对pagination的值进行监控,将分页参数发送到后台
- 运行观察效果
http://manage.leyou.com/#/item/brand
通过上述发送的请求编写后端,并返回当前页面的数据和总条数
2、 后台提供查询接口
(1)数据库
前台页面已经准备好,接下来就是后台提供数据接口了
CREATE TABLE `tb_brand` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '品牌id',
`name` varchar(50) NOT NULL COMMENT '品牌名称',
`image` varchar(200) DEFAULT '' COMMENT '品牌图片地址',
`letter` char(1) DEFAULT '' COMMENT '品牌的首字母',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=325402 DEFAULT CHARSET=utf8 COMMENT='品牌表,一个品牌下有多个商品(spu),一对多关系';
简单的四个字段
这里需要注意的是,品牌和商品分类之间是多对多关系。因此我们有一张中间表,来维护两者间关系;
DROP TABLE IF EXISTS `tb_category_brand`;
CREATE TABLE `tb_category_brand` (
`category_id` bigint(20) NOT NULL COMMENT '商品类目id',
`brand_id` bigint(20) NOT NULL COMMENT '品牌id',
PRIMARY KEY (`category_id`,`brand_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品分类和品牌的中间表,两者是多对多关系';
但是,你可能会发现,这张表中并没有设置外键约束,似乎与数据库的设计范式不符。为什么这么做?
-
外键会严重影响数据库读写的效率
-
数据删除时会比较麻烦
在电商行业当中,性能是非常
(2)实体类
创建对应的实体类
package com.leyou.item.pojo;
import lombok.Data;
import tk.mybatis.mapper.annotation.KeySql;
import javax.persistence.Id;
import javax.persistence.Table;
@Data
@Table(name = "tb_brand")
public class Brand {
/*
useGeneratedKeys设置为 true 时,表示如果插入的表id以自增列为主键,
则允许 JDBC 支持自动生成主键,并可将自动生成的主键id返回。
useGeneratedKeys参数只针对 insert 语句生效,默认为 false;
*/
@Id
@KeySql(useGeneratedKeys = true)
private Long id;
private String name;//
private String image;
private Character letter;
}
(3)创建实体类对应Mapper
package com.leyou.item.mapper;
import com.leyou.item.pojo.Brand;
import tk.mybatis.mapper.common.Mapper;
public interface BrandMapper extends Mapper<Brand> {
}
(4)实体类对应的Service
(7)生成实体类对应的控制层
、修改页面的代码的请求路径(在前加上item)
b、创建返回结果的数据的对象PageResult类
在这里封装一个类用来表示分页结果
package com.leyou.common.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/*
@Data 生成getter,setter等函数
@NoArgsConstructor 生成无参构造函数
@AllArgsConstructor //生成全参数构造函数
*/
@Data
public class PageResult<T> {
private Long total;//总条数
private Integer totalPage;//总页数
private List<T> items;//当前页数据
public PageResult() {
}
public PageResult(Long total,List<T> items){
this.total = total;
this.items = items;
}
public PageResult(Long total, Integer totalPage, List<T> items) {
this.total = total;
this.totalPage = totalPage;
this.items = items;
}
}
c、完善BrandController
package com.leyou.item.web;
import com.leyou.common.vo.PageResult;
import com.leyou.item.pojo.Brand;
import com.leyou.item.pojo.Category;
import com.leyou.item.service.BrandService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("brand")
public class BrandController {
@Autowired
public BrandService brandService;
@GetMapping("page")
public ResponseEntity<PageResult<Brand>> queryBrandByPage(
//设置请求参数参数名称value对应页面上的name,defaultValue默认值 然后是参数类型和参数名称
//相当于Integer name = Intgert.paseInt(request.getParamate("name")); 其中("name")等价于value="name"
//Integer name等价于Integer page
//request=false(ture)表示前端的参数是否一定要传入。true表示必须传入参数,false表示可传或不传
@RequestParam(value = "page",defaultValue = "1") Integer page,
@RequestParam(value = "rows",defaultValue = "5") Integer rows,
@RequestParam(value = "sortBy",required = false) String sortBy,
@RequestParam(value = "desc",defaultValue = "flase") Boolean desc,
@RequestParam(value = "key",required = false) String key
){
//上面配置好5个参数
//编写业务代码
PageResult<Brand> result = brandService.queryBrandByPage(page,rows,sortBy,desc,key);
return ResponseEntity.ok(result);
}
}
d、创建对应商品没有查询到的错误异常
BRAND_NOT_FOUND(404,"品牌不存在"),
e、完善业务层代码
package com.leyou.item.service;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.leyou.common.enums.ExceptionEnum;
import com.leyou.common.exception.LyException;
import com.leyou.common.vo.PageResult;
import com.leyou.item.mapper.BrandMapper;
import com.leyou.item.pojo.Brand;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import tk.mybatis.mapper.entity.Example;
import java.util.List;
@Service
public class BrandService {
@Autowired
private BrandMapper brandMapper;
public PageResult<Brand> queryBrandByPage(Integer page, Integer rows, String sortBy,
Boolean desc, String key) {
//分页(过滤器拦截MyBatis在其后面拼接分页条件)
PageHelper.startPage(page,rows);//自动创建好分页的条件
//过滤
Example example = new Example(Brand.class);//设置条件并指定到那张表查询
if(StringUtils.isNotBlank(key))//判断只要key不为空
{
//过滤条件
// select * from tb_brand where name like "%%" or letter = '' limit page,rows order by desc
//设置条件
example.createCriteria().//创建条件,并在后面设置对应的条件
orLike("name","%"+key+"%").//第一个条件
orEqualTo("letter",key.toUpperCase());//第二个条件 (key.toUpperCase()变成大写)
}
//排序
if(StringUtils.isNotBlank(sortBy)){//如果不为空做排序
//当sortBy是id的时候根据id查询,当sortBy是字母的时候根据字母排序,
// 三元运算符判断desc如果是true则DESC否则是ASC
String orderByClause = sortBy+(desc ? " DESC":" ASC");
example.setOrderByClause(orderByClause);//设置OrderBy排序,(直接编写SQL语句)
}
//查询
List<Brand> list = brandMapper.selectByExample(example);
if(CollectionUtils.isEmpty(list)){
throw new LyException(ExceptionEnum.BRAND_NOT_FOUND);
}
PageResult pageResult = new PageResult();
pageResult.setItems(list);//设置数据
//解析分页结果
PageInfo<Brand> pageInfo = new PageInfo<Brand>(list);//得到分页信息
pageResult.setTotal(pageInfo.getTotal());//设置总条数
return pageResult;
}
}
f、如果想要在上述代码当中看到SQL语句,需要添加如下的配置
以下配置设置SQL语句输出在控制台
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
(8)运行项目并测试
http://manage.leyou.com/#/item/brand
成功获取到数据
并在控制台输出了SQL语句
(9)完善页面代码
全部前端代码
<template>
<div>
<v-layout class="px-4 pb-2">
<v-flex xs2>
<v-btn color="info" >新增品牌</v-btn>
</v-flex>
<v-spacer />
<v-flex xs4>
<v-text-field label="搜索" hide-details append-icon="search" v-model="key"> </v-text-field>
</v-flex>
</v-layout>
<v-data-table
:headers="headers"
:items="brands"
:pagination.sync="pagination"
:total-items="totalBrands"
:loading="loading"
class="elevation-1"
>
<template slot="items" slot-scope="props" >
<td class="text-xs-center">{{ props.item.id }}</td>
<td class="text-xs-center">{{ props.item.name }}</td>
<td class="text-xs-center">
<img :src="props.item.image"/>
</td>
<td class="text-xs-center">{{ props.item.letter }}</td>
<td class="text-xs-center">
<v-btn flat icon color="info">
<v-icon>edit</v-icon>
</v-btn>
<v-btn flat icon color="error">
<v-icon>delete</v-icon>
</v-btn>
</td>
</template>
</v-data-table>
</div>
</template>
<script>
export default {
name: "MyBrand",
data(){
return{
headers:[
{ text : "品牌ID",
value : "id",
align:'center',
sortable : true,
},
{ text : "品牌名称",
value : "name",
align:'center',
sortable : false, //设置名称不可排序
},
{ text : "品牌LOGO",
value : "image",
align:'center',
sortable : false, //设置品牌LOGO不可排序
},
{ text : "品牌首字母",
value : "letter",
align:'center',
sortable : true,
},
{ text : "操作",
align:'center',
sortable : false,
}
],
brands:[],
pagination:{},
totalBrands:0,
loading:false, //设置初始不显示加载数据的动画
key:"", //搜索条件
}
},
created() {//编写构造函数
this.brands = [
//占时先编数据,后期从后台加载
{id:1, name: "OPPO",image : "1.jpg", letter:"X" },
{id:262626, name: "飞利浦",image : "11.jpg", letter:"F" },
{id:788, name: "华为",image : "21.jpg", letter:"H" },
{id:2122, name: "小米",image : "31.jpg", letter:"X" },
{id:3139, name: "魅族",image : "41.jpg", letter:"M" },
];
this.totalBrands = 15;
},
// 去后台查询
watch:{//设置监控,一旦key的值发送改变就发送请求从新加载数据
key(){
this.loadBrands();
},
//深度监控
pagination:{
deep:true,
handler(){
this.loadBrands();
}
}
},
methods:{
loadBrands(){
//设置开始发送请求加载进度条
this.loading = true;
this.$http.get("/item/brand/page",{
params:{
//分页参数
page: this.pagination.page,//这个页数是Vuetify帮我们以及提供好的分页
rows: this.pagination.rowsPerPage,//Vuetify提供的行数
sortBy: this.pagination.sortBy,//排序字段
desc:this.pagination.descending,//Vuetify提供的排序方式(是否降序)
//搜索条件
key: this.key //搜索条件后面的key是和v-model绑定的数据
}
}).then( resp=>{
console.log(resp);
this.brands = resp.data.items;
this.totalBrands = resp.data.total;
//设置结束发送请求不加载进度条
this.loading = false;
})
}
}
}
</script>
<style scoped>
</style>
重新运行测试
http://manage.leyou.com/#/item/brand
(10)完善一下搜索功能
this.pagination.page = 1;//每次修改条件的时候都将起始页设置为1
3、品牌新增功能
(1)页面搭建
a、初步编写弹框
当我们点击新增按钮,应该出现一个弹窗,然后在弹窗中出现一个表格,我们就可以填写品牌信息了。我们查看Vuetify官网,弹窗是如何实现:
<!--弹出的对话框-->
<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>
将这段代码复制到页面当中
接下来,我们要在点击新增品牌按钮时,将窗口显示,因此要给新增按钮绑定事件
<v-btn color="primary" @click="addBrand">新增品牌</v-btn>
然后定义一个addBrand方法:
addBrand(){
// 控制弹窗可见:
this.show = true;
}
效果:
窗口关闭
现在,悲剧发生了,因为我们设置了persistent属性,窗口无法被关闭了。除非把show属性设置为false
因此我们需要给窗口添加一个关闭按钮:
并且,我们还给按钮绑定了点击事件,回调函数为closeWindow。
接下来,编写closeWindow函数:
closeWindow(){
// 关闭窗口
this.show = false;
}
效果:
b、新增品牌的表单页
接下来就是写表单了。我们有两种选择:
- 直接在dialog对话框中编写表单代码
- 另外编写一个组件,组件内写表单代码。然后在对话框引用组件
选第几种?
我们选第二种方案,优点:
- 表单代码独立组件,可拔插,方便后期的维护。
- 代码分离,可读性更好。
我们新建一个MyBrandForm.vue
组件:
将MyBrandForm引入到MyBrand中,这里使用局部组件的语法:
// 导入自定义的表单组件
import MyBrandForm from './MyBrandForm'
修改一下表单的名称以及添加一些内容
<my-brand-form @close="closeWindow" ></my-brand-form>
然后通过components属性来指定局部组件:
components:{
MyBrandForm
}
c、编写表单
i、表单
查看官网文档,找到表单相关内容
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>
修改会原来的路由规则
ii、文本框
我们的品牌总共需要这些字段:
- 名称
- 首字母
- 商品分类,有很多个
- 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:显示后缀
接下来,我们先添加两个字段:品牌名称、品牌的首字母,校验规则暂时不写:
iii、级联下拉菜单
接下来就是商品分类了,按照刚才的分析,商品分类应该是下拉选框。
但是大家仔细思考,商品分类包含三级。在展示的时候,应该是先由用户选中1级,才显示2级;选择了2级,才显示3级。形成一个多级分类的三级联动效果。
这个时候,就不是普通的下拉选框,而是三级联动的下拉选框!
这样的选框,在Vuetify中并没有提供(它提供的是基本的下拉框)。因此我已经给大家编写了一个无限级联动的下拉选框,能够满足我们的需求。
<v-cascader
url="/item/category/list"
multiple
required
v-model="brand.categories"
label="请选择商品分类"/>
- url:加载商品分类选项的接口路径
- multiple:是否多选,这里设置为true,因为一个品牌可能有多个分类
- requried:是否是必须的,这里为true,会在提示上加*,提醒用户
- v-model:关联我们brand对象的categories属性
- label:文字说明
演示效果如下
iV、文件上传项
在Vuetify中,也没有文件上传的组件。
还好,我已经给大家写好了一个文件上传的组件:
我们添加上传的组件:
<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图片上传后展示的宽高
最终结果:
V、按钮
上面已经把所有的表单项写完。最后就差提交和清空的按钮了。
在表单的最下面添加两个按钮:
<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
属性:
<v-form v-model="valid" ref="myBrandForm">
clear(){
// 重置表单
this.$refs.myBrandForm.reset();//获取带ref对应的表单数据
//设置商品分类为空
this.categories = [];
}
要注意的是,这里我们还手动把this.categories清空了,因为我写的级联选择组件并没有跟表单结合起来。需要手动清空。
(2)表单校验
Vuetify的表单校验,是通过rules属性来指定的:
<v-text-field v-model="brand.name" label="请输入品牌名称" required :rules="nameRules" />
<v-text-field v-model="brand.letter" label="请输入品牌首字母" required :rules="letterRules" />
校验规则的写法:
nameRules: [
v => !!v || "品牌名称不能为空",
v => v.length > 1 || "品牌名称至少2位"
],
letterRules: [
v => !!v || "首字母不能为空",
v => /^[a-zA-Z]{1}$/.test(v) || "品牌字母只能是1个字母"
]
效果:
(3)提交表单信息
在submit方法中添加表单提交的逻辑:
submit(){
// 提交表单
if(this.$refs.myBrandForm.validate()){
//定义一个请求参数的对象,通过结构表达式来获取brand当中的属性
const { categories,letter, ...params } = this.brand;
//数据库当中只需要保留分类id即可,因此我们对categories的值进行处理,只保留id,并转换为字符串
params.cids = categories.map(c => c.id).join(",");
//将字母都处理为大写
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 -
设置在提交成功以后关闭窗口
//关闭窗口
this.$emit("close");
(4)完善后台接收表单提交的数据,保存
/*
新增品牌
*/
@PostMapping
public ResponseEntity<Void> saveBrand(Brand brand, @RequestParam("cids") List<Long> cids){
//ResponseEntity<Void>表示该方法无返回值
brandService.save(brand,cids);
//返回成功的状态码201,有返回值返回body没有则返回build
return ResponseEntity.status(HttpStatus.CREATED).build();
}
(5)完善业务层BrandService
- 在BrandService当中创建对应的save方法
public void save(Brand brand, List<Long> cids) {
//新增品牌
brand.setId(null);
int count = brandMapper.insert(brand);
}
- 编写新增品牌如果新增失败侧抛出异常
package com.leyou.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter //get方法
@NoArgsConstructor //无参构造
@AllArgsConstructor //有参构造
public enum ExceptionEnum {//枚举是只具有固定实例个数的类
PRICE_CANNOT_BE_NULL(400,"价格不能为空!"),
CATEGORY_NOT_FOND(404,"商品分类没有查到"),
BRAND_NOT_FOUND(404,"品牌不存在"),
BRAND_SAVE_ERROR(500,"新增匹配失败"),
;
private int code;
private String msg;
}
- 继续完善BrandService,当存储失败的时候抛出异常
public void save(Brand brand, List<Long> cids) {
//新增品牌
brand.setId(null);
int count = brandMapper.insert(brand);
if(count != 1){
throw new LyException(ExceptionEnum.BRAND_SAVE_ERROR);
}
}
- 以及新增中间表的数据层
package com.leyou.item.mapper;
import com.leyou.item.pojo.Brand;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import tk.mybatis.mapper.common.Mapper;
public interface BrandMapper extends Mapper<Brand> {
@Insert("insert into tb_category_brand (category_id,brand_id) values (#{cid},#{bid})")
int insertCategoryBrand(@Param("cid") Long cid, @Param("bid") Long bid);
}
- 完善BrandService业务层
public void save(Brand brand, List<Long> cids) {
//新增品牌
brand.setId(null);
int count = brandMapper.insert(brand);
if(count != 1){
throw new LyException(ExceptionEnum.BRAND_SAVE_ERROR);
}
//上诉在新增完数据以后会自动回显数据,就是会查询出当前插入对象的完整数据,
// 因此虽然id上诉的时候为空,但是在插入数据以后其中的id的数据会补全
for(Long cid : cids){
count = brandMapper.insertCategoryBrand(cid,brand.getId());
if(count != 1){
throw new LyException(ExceptionEnum.BRAND_SAVE_ERROR);
}
}
}
- 完善BrandService业务层,并添加事务
@Transactional
- 重新启动测试运行
- 提交表单数据失败
修改
this.$qs.stringify(params) //qs.stringify()将对象 序列化成URL的形式,以&进行拼接
- 再次提交数据