https://v15.vuetifyjs.com/zh-Hans/getting-started/quick-start
商品分类完成以后,自然轮到了品牌功能了。
分析左侧菜单的数据:
先看看我们要实现的效果:
接下来,我们从0开始,实现下从前端到后端的完整开发。
提供自定义品牌菜单项
在menu.js中添加菜单名称
在路由router.js指定菜单path所跳转的页面
在对应跳转路径下新建一个MyBrand.vue空文件
MyBrand.vue的内容:
<template>
<div>我的品牌页面div>
template>
03、从零开始制作页面:品牌列表展示
参考文档: https://v15.vuetifyjs.com/en/getting-started/quick-start
1)找到一个服务端分页组件点进去源码
分别复制template和script到MyBrand.vue页面中,稍微改动了一点点。
{{ props.item.name }}
{{ props.item.calories }}
{{ props.item.fat }}
{{ props.item.carbs }}
{{ props.item.protein }}
{{ props.item.iron }}
2)页面数据分析【data】
大家注意:
我们看一个vue页面,一定是从script的data中开始。
整个页面要用到的所有数据,都必须在data中定义。
其中data定义的变量的值的来源又分几类:
收集template页面数据到data中
接收后台响应的数据到data中
纯定义数据在页面使用的
查看data中的数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9T0xQ4gF-1652332569645)(assets/image-20200314094318516.png)]
把表头信息改成我们自己的品牌表的字段
data () {
return {
totalDesserts: 0,//总记录数
desserts: [],//当前页的数据列表
loading: true,//加载进度条的特效
pagination: {},//分页插件
headers: [//表头信息
{ text: '品牌编号', align: 'center', value: 'id'},
{ text: '品牌名称', align: 'center', value: 'name' },
{ text: '品牌图片', align: 'center', sortable: false, value: 'image' },
{ text: '品牌字母', align: 'center', value: 'letter' }
]
}
},
修改后页面的效果为:
3)渲染列表数据【通过钩子函数获取到要渲染的数据】上面,我们已经在data中定义好页面使用的数据了。
接下来,我们要在钩子函数中,向服务器发起请求,并获取数据列表和分页信息。
首先,模拟服务器返回的数据
[
{id: 2032, name: "OPPO", image: "1.jpg", letter: "O"},
{id: 2033, name: "飞利浦", image: "2.jpg", letter: "F"},
{id: 2034, name: "华为", image: "3.jpg", letter: "H"},
{id: 2036, name: "酷派", image: "4.jpg", letter: "K"},
{id: 2037, name: "魅族", image: "5.jpg", letter: "M"}
]
品牌中有id,name,image,letter字段。把这些数据放入页面中。
再次查看页面效果:
在template中找到哪个地方使用了数据列表的变量
我们已经知道凡是":"号开头的属性,都是可以识别vue数据的属性。这里这个:item就是分页列表插件接收列表数据的属性,具体遍历在下面这段代码中:
此刻页面效果为:
第一步:在data中定义数据
第二步:在钩子函数中发起请求
第三步:如果请求需要参数,在data中直接拿,不需要参数则忽略步骤
第四步:发起服务器请求,在回调函数中,把服务器返回的数据,赋值给data中定义的变量
第五步:直接在template中渲染数据
04、从零开始制作页面:添加按钮和搜索框在table上面加入如下代码:
<v-layout row wrap>
<v-flex xs6>
<v-btn round color="primary" dark>品牌添加v-btn>
v-flex>
<v-flex xs6>
<v-text-field
label="搜索"
prepend-icon="search"
>v-text-field>
v-flex>
v-layout>
此刻效果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dvjlrdv4-1652332569647)(assets/image-20200314103420235.png)]
MyBrand.vue页面
<template>
<div>
<v-layout row wrap>
<v-flex xs6>
<v-btn color="primary" round>品牌添加v-btn>
v-flex>
<v-flex xs6>
<v-text-field
label="搜索"
prepend-icon="search"
>v-text-field>
v-flex>
v-layout>
<v-data-table
:headers="headers"
:items="desserts"
:pagination.sync="pagination"
:total-items="totalDesserts"
:loading="loading"
class="elevation-1"
>
<template v-slot:items="props">
<td class="text-xs-center">{{ props.item.id }}td>
<td class="text-xs-center">{{ props.item.name }}td>
<td class="text-xs-center">{{ props.item.image }}td>
<td class="text-xs-center">{{ props.item.letter }}td>
template>
v-data-table>
div>
template>
<script>
export default {
data () {
return {
totalDesserts: 0,
desserts: [],
loading: false,
pagination: {},
headers: [
{
text: '品牌编号',
align: 'center',
sortable: true
},
{
text: '品牌名称',
align: 'center',
sortable: false
},
{
text: '品牌图片',
align: 'center',
sortable: false
},
{
text: '品牌首字母',
align: 'center',
sortable: true
}
]
}
},
watch: {
pagination: {
handler () {
this.getDataFromApi()
.then(data => {
this.desserts = data.items
this.totalDesserts = data.total
})
},
deep: true
}
},
mounted () {
this.getDataFromApi()
.then(data => {
this.desserts = data.items
this.totalDesserts = data.total
})
},
methods: {
getDataFromApi () {
this.loading = false
return new Promise((resolve, reject) => {
const { sortBy, descending, page, rowsPerPage } = this.pagination
let items = this.getDesserts()
const total = items.length
if (this.pagination.sortBy) {
items = items.sort((a, b) => {
const sortA = a[sortBy]
const sortB = b[sortBy]
if (descending) {
if (sortA < sortB) return 1
if (sortA > sortB) return -1
return 0
} else {
if (sortA < sortB) return -1
if (sortA > sortB) return 1
return 0
}
})
}
if (rowsPerPage > 0) {
items = items.slice((page - 1) * rowsPerPage, page * rowsPerPage)
}
setTimeout(() => {
this.loading = false
resolve({
items,
total
})
}, 1000)
})
},
getDesserts () {
return [
{id: 2032, name: "OPPO", image: "1.jpg", letter: "O"},
{id: 2033, name: "飞利浦", image: "2.jpg", letter: "F"},
{id: 2034, name: "华为", image: "3.jpg", letter: "H"},
{id: 2036, name: "酷派", image: "4.jpg", letter: "K"},
{id: 2037, name: "魅族", image: "5.jpg", letter: "M"}
]
}
}
}
script>
05、从零开始制作页面:改为服务端分页
1)删除客户端分页的代码
虽然我们用的是服务端分页的插件,但是vuetify为了便于前端工作人员,可以更好的调整页面样式,给我们临时做了一个页面效果出来,我们要去掉这个效果,其实就是删除getDataFromApi方法,并删除相关连带的部分,最终代码如下:
品牌添加
{{ props.item.id }}
{{ props.item.name }}
{{ props.item.image }}
{{ props.item.letter }}
2)自己来定义一个服务端分页的请求方法
在methods中定义方法:
loadBrandData(){
this.desserts = this.getDesserts ()//给当前data中的数据列表赋值
this.totalDesserts = 20 //给总数据量赋值
},
在钩子函数中调用上面的方法:
mounted () {
//发起服务端分页请求
this.loadBrandData()
},
此刻最终代码如下:
品牌添加
{{ props.item.id }}
{{ props.item.name }}
{{ props.item.image }}
{{ props.item.letter }}
06、从零开始制作页面:向后端请求品牌分页数据
说明
截至目前,我们的品牌静态页面已经做好了,接下来,就是vue代码部分了!
使用vue把静态页面变成动态页面的步骤如下:
第一步:参考开发api文档 第二步:根据api要求提供要向后台传输的参数在data中定义一个变量接收搜索数据
在页面上使用双向绑定给searchKey赋值
如果我们使用了vuetify,那么所有分页相关的数据,无需自己在data中定义,在data中的pagination是空对象,但是我们用vue的chrome插件查看页面的数据发现,里面都已经包含了所有的分页条件,如下:
条件如下:
{"descending":false,"page":1,"rowsPerPage":5,"sortBy":"id","totalItems":0}
我们在代码中并没有给pagination赋值,但是发现打开页面它就自己有值了,所以是框架给我们赋的值,也就是说这些数据我们都不用关心了,我们只需要把后台的数据正确返回页面就能显示了。
第三步:页面发起后台服务器请求修改向后台发起请求的方法:
methods: {
//加载后台品牌数据
loadBrandData(){
//ajax异步请求后台
this.$http.get('/item/brand/page',{
params:{
page:this.pagination.page,
rows:this.pagination.rowsPerPage,
key:this.searchKey,
sortBy:this.pagination.sortBy,
desc:this.pagination.descending
}
}).then(resp=>{
this.desserts = this.getDesserts();
this.totalDesserts = 20;
//关闭进度条
this.loading = false;
}).catch(e=>{
console.log('加载品牌数据失败');
})
},
getDesserts () {
return [
{id: 2032, name: "OPPO", image: "1.jpg", letter: "O"},
{id: 2033, name: "飞利浦", image: "2.jpg", letter: "F"},
{id: 2034, name: "华为", image: "3.jpg", letter: "H"},
{id: 2036, name: "酷派", image: "4.jpg", letter: "K"},
{id: 2037, name: "魅族", image: "5.jpg", letter: "M"}
]
}
}
通过页面f12在网络中查看效果
07、从零开始制作页面:监听分页参数与查询条件添加watch的监听事件,由于分页参数是一个对象,所以我们需要使用深度监听。
在Vue中添加watch事件
watch:{
//监听searchKey的变化
"searchKey":{
handler(){
this.loadBrandData();
}
},
"pagination":{
deep:true, //注意:如果vue需要监听的是对象的属性变化,必须使用深度监听
handler(){
this.loadBrandData();
}
}
},
最终页面代码为:
品牌添加
{{ props.item.id }}
{{ props.item.name }}
{{ props.item.image }}
{{ props.item.letter }}
08、编写品牌后台接口:品牌模块的准备工作
1)通用的分页对象
我们把这个对象放到 ly-common 模块的pojo包中。
package com.leyou.common.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 通用的分页封装对象
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult<T> {
private Long total;//总记录数
private Long totalPage;//总页数
private List<T> items;
}
2)tb_brand表的基本类
用逆向工程生成代码,并将实体类放到ly-pojo-item下
这边发现上次导入之后无法生成代码,原因是因为2个版本的mybatis版本核心包不一致。
将他改成3.4.1版本即可
## 09、编写品牌后台接口:品牌分页查询(**)
### 1) 独立创建MybatisPlus分页拦截器(*)
编写分页配置类
```java
package com.leyou.item.config;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 分页插件配置类
*/
@Configuration
public class PageHelperConfiguration {
/**
* 创建分页插件拦截器对象
*/
@Bean
public PaginationInterceptor paginationInterceptor(){
return new PaginationInterceptor();
}
}
2)编写处理器
package com.leyou.item.controller;
import com.leyou.common.pojo.PageResult;
import com.leyou.item.pojo.Brand;
import com.leyou.item.service.BrandService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BrandController {
@Autowired
private BrandService brandService;
/**
* 分页查询品牌
*/
@GetMapping("/brand/page")
public ResponseEntity<PageResult<Brand>> brandPageQuery(
@RequestParam(value = "page",defaultValue = "1") Integer page,
@RequestParam(value = "rows",defaultValue = "5") Integer rows,
@RequestParam(value = "key",required = false) String key,
@RequestParam(value = "sortBy",required = false) String sortBy,
@RequestParam(value = "desc",required = false) Boolean desc
){
PageResult<Brand> pageResult = brandService.brandPageQuery(page,rows,key,sortBy,desc);
return ResponseEntity.ok(pageResult);
}
}
3)创建Service
package com.leyou.item.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.leyou.common.pojo.PageResult;
import com.leyou.item.mapper.BrandMapper;
import com.leyou.item.pojo.Brand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class BrandService {
@Autowired
private BrandMapper brandMapper;
public PageResult<Brand> brandPageQuery(Integer page, Integer rows, String key, String sortBy, Boolean desc) {
//1.封装条件
//1.1 IPage: 这里用于封装分页前的参数
IPage<Brand> iPage = new Page<>(page,rows);
//1.2 QueryWrapper: 用于封装查询条件(也可以排序)
QueryWrapper<Brand> queryWrapper = Wrappers.query(); //待会自己往QueryWrapper存入条件
//处理key
if(StringUtils.isNotEmpty(key)){
//sql: where name like '%H%' or letter = 'H'
/**
* 参数一:字段名称
* 注意:like方法的value已经包含%xxx%
*/
queryWrapper
.like("name",key)
.or()
.eq("letter",key.toUpperCase());
}
//处理排序
if (StringUtils.isNotEmpty(sortBy)) {
if(desc){
//降序
queryWrapper.orderByDesc(sortBy);
}else{
//升序
queryWrapper.orderByAsc(sortBy);
}
}
//2.执行查询,获取结果
//IPage: 这里的IPage封装分页后的结果
iPage = brandMapper.selectPage(iPage,queryWrapper);
//3.处理并返回结果
//3.1 封装PageResult
PageResult<Brand> pageResult = new PageResult<>(iPage.getTotal(),iPage.getPages(),iPage.getRecords());
//3.2 返回
return pageResult;
}
}
4)重启ly-item测试
10、品牌查询:渲染品牌分页查询页面
1)页面接收服务器返回的分页数据
先通过页面f12的控制台查看回调函数中的resp结果的结构是如何的:
由resp的数据结构得知,回调函数的代码应该为:
品牌添加
{{ props.item.id }}
{{ props.item.name }}
{{ props.item.letter }}
11、新增品牌:思路分析
品牌查询已经做好了,那么新增品牌我们直接在Brand.vue文件中开发即可。
品牌数据如何添加? 除了自己的数据,还有无其他数据?
之前分析过品牌表和分类表是多对多,分类的数据,一般变动的比较少,所以中间表我们一般让变动比较多的品牌表来维护,也就是说我们添加完品牌数据,还需要在中间表添加数据。
外键:
逻辑外键:通过代码维护关系,
优点:便于迁移
缺点:出现脏数据
物理外键:数据库维护关系,
优点:数据完整
缺点:不方便删除
品牌添加页面分析图:
由品牌添加页面分析得知,品牌添加步骤分三步:
第一:先把图片存入到图片服务器
第二:把品牌对象入库
第三:维护品牌和分类的中间表
12、新增品牌:完成品牌的保存和中间表的维护 1)编写Controller /**
* 新增品牌
* 1) 使用对象接收的情况
* Brand brand: 用于接收页面的普通参数,例如:name=xxx&letter=C&image=xxx
* @RequestBody Brand brand: 用于接收页面的json参数,例如:{name:"xxx",letter:"C",image:"xxx"}
*
* 2)接收页面的同名参数
* 页面上有两种情况传递的同名参数
* 1)复选框提交的格式 例如 ids=1&ids=2&ids=3....
* 2)使用逗号拼接格式 例如 ids=1,2,3....
* 后台如何接收同名参数
* 1)字符串 String ids 内容:1,2,3....
* 2) 数组 Long[] ids 内容:[1,2,3]
* 3)List集合 List ids 内容:[1,2,3] 注意:List集合必须添加@RequestParam注解
*/
@PostMapping("/brand")
public ResponseEntity<Void> saveBrand(
Brand brand,
@RequestParam("cids") List<Long> cids
){
brandService.saveBrand(brand,cids);
//return ResponseEntity.status(HttpStatus.CREATED).body(null);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
2)编写Service
这里要注意,我们不仅要新增品牌,还要维护品牌和商品分类的中间表。
public void saveBrand(Brand brand, List<Long> cids) {
try {
//1.保存品牌表数据
brandMapper.insert(brand); // insert()方法自动把数据库自增id值赋给Brand的id
//2.保存分类品牌中间表数据
brandMapper.saveCategoryAndBrand(brand.getId(),cids);
} catch (Exception e) {
e.printStackTrace();
throw new LyException(ExceptionEnum.INSERT_OPERATION_FAIL);
}
}
3)编写Mapper
package com.leyou.item.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.leyou.item.pojo.Brand;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface BrandMapper extends BaseMapper<Brand> {
void saveCategoryAndBrand(@Param("bid") Long id,@Param("cids") List<Long> cids);
}
在resource下新建一个目录:mappers
,并在下面新建文件:BrandMapper.xml
然后在application.yml
文件中配置mapper文件的地址:
mybatis-plus:
type-aliases-package: com.leyou.item.pojo
configuration:
map-underscore-to-camel-case: true
mapper-locations: classpath:mappers/*.xml #修改Mapper映射文件的路径
在BrandMapper.xml
中定义Sql的statement:
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.leyou.item.mapper.BrandMapper">
<insert id="saveCategoryAndBrand">
INSERT INTO tb_category_brand(category_id,brand_id) VALUES
<foreach collection="cids" item="cid" separator=",">
(#{cid},#{bid})
foreach>
insert>
mapper>
编写完成代码后,重启微服务,测试查看品牌和中间表的数据是否已经插入即可!
4)开启myBatis日志
# 开启日志
logging:
level:
com.leyou: debug
13、总结
1)品牌分页列表
1.1 了解页面使用Vuetify组件完成基本页面制作
1.2 熟悉前端ajax请求后台过程!!!!(请求方式,路径,参数,返回值)
1.3 完成品牌列表展示(MyBatis-Plus 带条件分页)
2)品牌添加
2.1 *** 作两种表,品牌表,品牌分类表
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)