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的形式,以&进行拼接
  • 再次提交数据
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐