02、从零开始制作页面:添加品牌菜单
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页面中,稍微改动了一点点。
<template>
<div>
<v-data-table
:headers="headers"
:items="desserts"
:pagination.sync="pagination"
:total-items="totalDesserts"
:loading="loading"
class="elevation-1"
>
<!--
slot="items" 指定要遍历的数据
slot-scope="props" 把当前遍历的数据起个别名
-->
<template slot="items" slot-scope="props">
<td>{{ props.item.name }}</td>
<td class="text-xs-right">{{ props.item.calories }}</td>
<td class="text-xs-right">{{ props.item.fat }}</td>
<td class="text-xs-right">{{ props.item.carbs }}</td>
<td class="text-xs-right">{{ props.item.protein }}</td>
<td class="text-xs-right">{{ props.item.iron }}</td>
</template>
</v-data-table>
</div>
</template>
<script>
export default {
data () {
return {
totalDesserts: 0,
desserts: [],
loading: true,
pagination: {},
headers: [
{
text: 'Dessert (100g serving)',
align: 'left',
sortable: false,
value: 'name'
},
{ text: 'Calories', value: 'calories' },
{ text: 'Fat (g)', value: 'fat' },
{ text: 'Carbs (g)', value: 'carbs' },
{ text: 'Protein (g)', value: 'protein' },
{ text: 'Iron (%)', value: 'iron' }
]
}
},
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 = true
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 [
{
name: 'Frozen Yogurt',
calories: 159,
fat: 6.0,
carbs: 24,
protein: 4.0,
iron: '1%'
},
{
name: 'Ice cream sandwich',
calories: 237,
fat: 9.0,
carbs: 37,
protein: 4.3,
iron: '1%'
},
{
name: 'Eclair',
calories: 262,
fat: 16.0,
carbs: 23,
protein: 6.0,
iron: '7%'
},
{
name: 'Cupcake',
calories: 305,
fat: 3.7,
carbs: 67,
protein: 4.3,
iron: '8%'
},
{
name: 'Gingerbread',
calories: 356,
fat: 16.0,
carbs: 49,
protein: 3.9,
iron: '16%'
},
{
name: 'Jelly bean',
calories: 375,
fat: 0.0,
carbs: 94,
protein: 0.0,
iron: '0%'
},
{
name: 'Lollipop',
calories: 392,
fat: 0.2,
carbs: 98,
protein: 0,
iron: '2%'
},
{
name: 'Honeycomb',
calories: 408,
fat: 3.2,
carbs: 87,
protein: 6.5,
iron: '45%'
},
{
name: 'Donut',
calories: 452,
fat: 25.0,
carbs: 51,
protein: 4.9,
iron: '22%'
},
{
name: 'KitKat',
calories: 518,
fat: 26.0,
carbs: 65,
protein: 7,
iron: '6%'
}
]
}
}
}
</script>
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就是分页列表插件接收列表数据的属性,具体遍历在下面这段代码中:
此刻页面效果为:
4)总结vue页面渲染数据的流程
第一步:在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>
<!--
headers:定义表格的表头
items:定义表格的内容
pagination: 定义表格的分页条内容和排序内容(注意:该对象不需要我们控制,组件自动复制)
total-items:定义总记录数
loading: 进度条
-->
<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方法,并删除相关连带的部分,最终代码如下:
<template>
<div>
<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>
<v-data-table
:headers="headers"
:items="desserts"
:pagination.sync="pagination"
:total-items="totalDesserts"
:loading="loading"
class="elevation-1"
>
<!--
slot="items" 指定要遍历的数据
slot-scope="props" 把当前遍历的数据items起个别名
props中有个固定的属性叫item,是props遍历时的临时变量
也就是说item就相当于一个品牌对象了
-->
<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">{{ 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: 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' }
]
}
},
mounted () {
},
methods: {
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: 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: 2036, name: "酷派", image: "4.jpg", letter: "K"},
{id: 2036, name: "酷派", image: "4.jpg", letter: "K"},
{id: 2036, name: "酷派", image: "4.jpg", letter: "K"},
{id: 2037, name: "魅族", image: "5.jpg", letter: "M"}
]
}
}
}
</script>
2)自己来定义一个服务端分页的请求方法
在methods中定义方法:
loadBrandData(){
this.desserts = this.getDesserts ()//给当前data中的数据列表赋值
this.totalDesserts = 20 //给总数据量赋值
},
在钩子函数中调用上面的方法:
mounted () {
//发起服务端分页请求
this.loadBrandData()
},
此刻最终代码如下:
<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>
<!--
headers:定义表格的表头
items:定义表格的内容
pagination: 定义表格的分页条内容和排序内容(注意:该对象不需要我们控制,组件自动复制)
total-items:定义总记录数
loading: 进度条
-->
<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
}
]
}
},
mounted () {
//调用列表查询
this.loadBranData();
},
methods: {
//后台加载品牌列表数据
loadBranData(){
// 异步请求
this.desserts = this.getDesserts();
this.totalDesserts = 5;
},
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>
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();
}
}
},
最终页面代码为:
<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-model="searchKey"
></v-text-field>
</v-flex>
</v-layout>
<!--
headers: 定义表头信息
items: 定义表的内容(数据列表) ***
pagination: 定义分页参数数据 ***
total-items: 总记录数 ***
loading: 进度条效果
-->
<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: true,
pagination: {},
headers: [
{ text: '品牌编号', value: 'id' , align:'center'},
{ text: '品牌名称', value: 'name', align:'center' ,sortable:false },
{ text: '品牌图片', value: 'image', align:'center' ,sortable:false },
{ text: '品牌字母', value: 'letter', align:'center' },
],
searchKey:'',//搜索关键词
}
},
mounted () {
//调用分页查询品牌
this.loadBrandData();
},
//监听
watch:{
"searchKey":{
handler(){
this.loadBrandData();
}
},
"pagination":{
deep:true,//深度监听。注意:监听对象里面的属性变化时用到
handler(){
this.loadBrandData();
}
}
},
methods: {
//分页查询品牌
loadBrandData(){
//请求后台
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;
}).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"}
]
}
}
}
</script>
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.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author yy
* 分页插件配置类
*/
@Configuration
public class PageHelperConfiguration {
/**
* 创建分页拦截器对象
* @return
*/
@Bean
public MybatisPlusInterceptor MybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor();
// 设置请求的页面大于最大页后操作,true调回到首页,false继续请求。默认false
pageInterceptor.setOverflow(false);
// 单页分页条数限制,默认无限制
pageInterceptor.setMaxLimit(500L);
// 设置数据库类型
pageInterceptor.setDbType(DbType.MYSQL);
interceptor.addInnerInterceptor(pageInterceptor);
return interceptor;
}
}
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测试
重启getway
10、品牌查询:渲染品牌分页查询页面
1)页面接收服务器返回的分页数据
先通过页面f12的控制台查看回调函数中的resp结果的结构是如何的:
由resp的数据结构得知,回调函数的代码应该为:
2)修改品牌列表中图片的显示
3)最终页面代码
<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-model="searchKey"></v-text-field>
</v-flex>
</v-layout>
<!--
headers: 定义表格的头部信息(*)
items: 定义表格的内容信息(**)
pagination: 定义分页相关数据(分页条数据,排序字段数据等)
total-items:总记录数(**)
loading: 控制进度条
-->
<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"><img :src="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: true,
pagination: {},
headers: [
{ text: '品牌编号', align: 'center' , value: 'id' },
{ text: '品牌名称', align: 'center' , sortable: false, value: 'name' },
{ text: '品牌图片', align: 'center' , sortable: false, value: 'image' },
{ text: '品牌字母', align: 'center' , value: 'letter' },
],
searchKey:'',//搜索关键词
}
},
mounted () {
this.loadBrandData();
},
watch:{
//监听searchKey的变化
"searchKey":{
handler(){
this.loadBrandData();
}
},
"pagination":{
deep:true, //注意:如果vue需要监听的是对象的属性变化,必须使用深度监听
handler(){
this.loadBrandData();
}
}
},
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 = resp.data.items;
this.totalDesserts = resp.data.total;
//关闭进度条
this.loading = false;
}).catch(e=>{
console.log('加载品牌数据失败');
})
}
}
}
</script>
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<Long> 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:
<?xml version="1.0" encoding="UTF-8" ?>
<!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">
<!--
collection: 需要遍历的数据(数组或集合)
item: 每个的元素的别名
separator: (可选)分隔符
-->
<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 操作两种表,品牌表,品牌分类表