前端Vue+ElementUI的Pagination分页组件实现分页展示 & 后端Spring Boot +Mybatis Plus实现分页接口
很久没有更新博客了,主要原因是博主一直在补充自己,发现自己还有很多地方不足,等博主补充好了,再来相互探讨技术。
主要是看到评论区的小伙伴问分页该怎么实现,博主就花了几个小时去实现这个小栗子。
演示
分页测试
数据库
有数据才能进行展示,为了简单,就一个product
表。
博主去年爬了一些商品数据,用于博主的本科毕业设计,这个product
表是项目数据库中的其中一个表,还有商品类目表、订单表、订单详情表、支付信息表、收货地址表、用户表、推荐表、推荐详情表,这里就不涉及了。
DROP TABLE IF EXISTS `product`;
CREATE TABLE `product` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '商品id',
`category_id` int(11) NOT NULL COMMENT '分类id,对应mall_category表的主键',
`name` varchar(100) NOT NULL COMMENT '商品名称',
`subtitle` varchar(200) DEFAULT NULL COMMENT '商品副标题',
`main_image` varchar(500) DEFAULT NULL COMMENT '产品主图,url相对地址',
`sub_images` text COMMENT '图片地址,json格式,扩展用',
`detail` text COMMENT '商品详情',
`price` decimal(20,2) NOT NULL COMMENT '价格,单位-元保留两位小数',
`stock` int(11) NOT NULL COMMENT '库存数量',
`status` int(6) DEFAULT '1' COMMENT '商品状态.1-在售 2-下架 3-删除',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
product
表中有7321
条商品数据,毕竟博主爬了几天,当时写的爬虫现在也被反爬了。
后端
先了解一下Mybatis Plus的功能和特性:
- MyBatis Plus汇总
项目代码结构:
-
pom.xml
:项目依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.kaven</groupId>
<artifactId>page</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>page</name>
<description>分页演示</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
-
application.yml
:配置文件,主要就是数据库的配置。
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
username: root
password: ITkaven
url: jdbc:mysql://ip_address:3306/system?characterEncoding=utf-8&useSSL=false
logging:
pattern:
console: "[%thread] %-5level %logger{36} - %msg%n"
server:
port: 8085
-
Product
:实体类,对应数据库中的商品表。
package com.kaven.page.pojo;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("product")
public class Product {
private Integer id;
private Integer categoryId;
private String name;
private String subtitle;
private String mainImage;
private String subImages;
private String detail;
private BigDecimal price;
private Integer stock;
private Integer status;
private Date createTime;
private Date updateTime;
}
-
ProductMapper
:商品的Mapper接口,继承Mybatis Plus的BaseMapper即可,对商品表的增删改查通过该商品Mapper接口即可实现。
package com.kaven.page.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kaven.page.pojo.Product;
import org.springframework.stereotype.Component;
@Component
public interface ProductMapper extends BaseMapper<Product> {}
-
MybatisPlusConfig
:配置Mybatis Plus的分页,通过给Mybatis Plus的拦截器加分页拦截器即可;overflow
(溢出总页数后是否进行处理)、dbType
(数据库类型)。
package com.kaven.page.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
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;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
paginationInnerInterceptor.setDbType(DbType.MYSQL);
paginationInnerInterceptor.setOverflow(true);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> configuration.setUseDeprecatedExecutor(false);
}
}
-
ResponseEnum
:响应状态信息的枚举类,提供一个响应状态信息的模板,如(100 , "库存不足")
等。
package com.kaven.page.enums;
import lombok.Getter;
@Getter
public enum ResponseEnum {
SUCCESS(0, "成功"),
;
Integer code;
String desc;
ResponseEnum(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
}
-
ProductVo
:一般不会将Product
类的实例响应给客户端,不然一些比较私密的数据就被泄露了,如商品库存等,所以这个类,就是后端可以响应给客户端的部分商品信息。
package com.kaven.page.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class ProductVo {
private Integer id;
private Integer categoryId;
private String name;
private String subtitle;
private String mainImage;
private BigDecimal price;
private Integer status;
}
-
ResponseVo
:响应类,有状态码、描述信息和数据(泛型)。
package com.kaven.page.vo;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.kaven.page.enums.ResponseEnum;
import lombok.Data;
@Data
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class ResponseVo<T> {
private Integer status;
private String msg;
private T data;
private ResponseVo(Integer status, T data) {
this.status = status;
this.data = data;
}
public static <T> ResponseVo<T> success(T data){
return new ResponseVo<T>(ResponseEnum.SUCCESS.getCode(), data);
}
}
-
IProductService
:服务接口。
package com.kaven.page.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.kaven.page.vo.ProductVo;
import com.kaven.page.vo.ResponseVo;
public interface IProductService {
ResponseVo<IPage<ProductVo>> products(Integer pageNum, Integer pageSize , Integer category);
}
-
ProductServiceImpl
:实现服务接口。
package com.kaven.page.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.kaven.page.dao.ProductMapper;
import com.kaven.page.pojo.Product;
import com.kaven.page.service.IProductService;
import com.kaven.page.vo.ProductVo;
import com.kaven.page.vo.ResponseVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class ProductServiceImpl implements IProductService {
@Autowired
private ProductMapper productMapper;
@Override
public ResponseVo<IPage<ProductVo>> products(Integer pageNum, Integer pageSize , Integer category) {
LambdaQueryWrapper<Product> productQuery = Wrappers.lambdaQuery();
productQuery.eq(Product::getCategoryId , category);
Page<Product> page = new Page(pageNum , pageSize);
IPage<Product> productIPage = productMapper.selectPage(page , productQuery);
IPage<ProductVo> productVoIPage = new Page<>();
BeanUtils.copyProperties(productIPage , productVoIPage);
List<ProductVo> productVoList = productIPage.getRecords().stream().map(
e->product2ProductVo(e)
).collect(Collectors.toList());
productVoIPage.setRecords(productVoList);
return ResponseVo.success(productVoIPage);
}
private ProductVo product2ProductVo(Product product){
ProductVo productVo = new ProductVo();
BeanUtils.copyProperties(product , productVo);
return productVo;
}
}
主要是Page
这个类,它实现了IPage
接口,实际上返回的是Page
类的实例,通过下面代码(删减版),可以看到该类有前端需要的一些数据,如total
(符合要求的商品总数)、records
(当前页符合要求的商品列表)等;如果该类无法满足我们的需求,我们可以自己实现IPage
接口,来实现一些定制化。
package com.baomidou.mybatisplus.extension.plugins.pagination;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
/**
* 简单分页模型
*
* @author hubin
* @since 2018-06-09
*/
public class Page<T> implements IPage<T> {
private static final long serialVersionUID = 8545996863226528798L;
/**
* 查询数据列表
*/
protected List<T> records = Collections.emptyList();
/**
* 总数
*/
protected long total = 0;
/**
* 每页显示条数,默认 10
*/
protected long size = 10;
/**
* 当前页
*/
protected long current = 1;
/**
* 排序字段信息
*/
@Getter
@Setter
protected List<OrderItem> orders = new ArrayList<>();
/**
* 自动优化 COUNT SQL
*/
protected boolean optimizeCountSql = true;
/**
* 是否进行 count 查询
*/
protected boolean isSearchCount = true;
/**
* 是否命中count缓存
*/
protected boolean hitCount = false;
/**
* countId
*/
@Getter
@Setter
protected String countId;
/**
* countId
*/
@Getter
@Setter
protected Long maxLimit;
public Page() {
}
/**
* 分页构造函数
*
* @param current 当前页
* @param size 每页显示条数
*/
public Page(long current, long size) {
this(current, size, 0);
}
public Page(long current, long size, long total) {
this(current, size, total, true);
}
public Page(long current, long size, boolean isSearchCount) {
this(current, size, 0, isSearchCount);
}
public Page(long current, long size, long total, boolean isSearchCount) {
if (current > 1) {
this.current = current;
}
this.size = size;
this.total = total;
this.isSearchCount = isSearchCount;
}
/**
* 是否存在上一页
*
* @return true / false
*/
public boolean hasPrevious() {
return this.current > 1;
}
/**
* 是否存在下一页
*
* @return true / false
*/
public boolean hasNext() {
return this.current < this.getPages();
}
@Override
public List<T> getRecords() {
return this.records;
}
@Override
public Page<T> setRecords(List<T> records) {
this.records = records;
return this;
}
@Override
public long getTotal() {
return this.total;
}
@Override
public Page<T> setTotal(long total) {
this.total = total;
return this;
}
@Override
public long getSize() {
return this.size;
}
@Override
public Page<T> setSize(long size) {
this.size = size;
return this;
}
@Override
public long getCurrent() {
return this.current;
}
@Override
public Page<T> setCurrent(long current) {
this.current = current;
return this;
}
@Override
public String countId() {
return getCountId();
}
@Override
public Long maxLimit() {
return getMaxLimit();
}
...
}
-
ProductController
:商品接口,注解@CrossOrigin
用于跨域。
package com.kaven.page.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.kaven.page.service.IProductService;
import com.kaven.page.vo.ProductVo;
import com.kaven.page.vo.ResponseVo;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class ProductController {
@Resource
private IProductService productService;
@GetMapping("/products")
@CrossOrigin
public ResponseVo<IPage<ProductVo>> recommend(@RequestParam(required = false , defaultValue = "1") Integer pageNum ,
@RequestParam(required = false , defaultValue = "10") Integer pageSize ,
@RequestParam Integer category){
return productService.products(pageNum , pageSize , category);
}
}
-
Application
:启动类。
package com.kaven.page;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan(basePackages = "com.kaven.page.dao")
public class Application {
public static void main(String[] args){
SpringApplication.run(Application.class, args);
}
}
后端就介绍完了,还是比较简单的。
前端
ElementUI的Pagination分页组件:
大家可以先看一下这个组件实现分页的效果怎么样。
看看下面这些内容,了解Vue的基本特性以及如何创建Vue项目:
- Vue汇总
- Vue - vue create、vue ui、vue init三种方式创建Vue项目
博主这里使用 vue create
命令创建的Vue项目(Vue2.x
)。
项目结构:
创建项目后,再下载一些依赖包:
"axios": "^0.21.1",
"element-ui": "^2.15.1",
"node-sass": "^4.14.1",
"sass-loader": "^8.0.0",
大版本要保持一致。
npm i -S axios@0.21.1
npm i -S element-ui@2.15.1
npm i -S sass-loader@8.0.0
npm i -S node-sass@4.14.1
-
i
:是install
的简写。 -
-S
:即--save
(保存),依赖包会被注册到package.json
的dependencies
里面。
对Vue项目文件比较陌生的话,可以看一下这篇博客:Vue项目 - 项目文件介绍。
-
main.js
:项目入口文件。
import Vue from 'vue'
import App from './App.vue'
import axios from 'axios'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
Vue.config.productionTip = false
axios.defaults.baseURL = 'http://localhost:8085';
axios.defaults.timeout = 8000;
Vue.prototype.$http = axios
new Vue({
render: h => h(App),
}).$mount('#app')
-
App.vue
:项目根组件。
<template>
<div id="app">
<Page/>
</div>
</template>
<script>
import Page from './components/Page.vue'
export default {
name: 'App',
components: {
Page
}
}
</script>
-
Page.vue
:分页展示组件。
<template>
<div class="page">
<div class="wrapper">
<div class="list-box">
<div class="list" v-for="(arr,i) in list" v-bind:key="i">
<div class="item" v-for="(item,j) in arr" v-bind:key="j">
<div class="item-img">
<img :src="item.mainImage" alt="">
</div>
<div class="item-info">
<h3>{{item.name}}</h3>
<p class="price">{{item.price}}元</p>
</div>
</div>
</div>
<el-pagination
class="pagination"
background
layout="prev, pager, next"
:pageSize="pageSize"
:total="total"
@current-change="change"
>
</el-pagination>
</div>
</div>
</div>
</template>
<script>
import {Pagination} from "element-ui";
export default{
components:{
[Pagination.name]:Pagination,
},
data() {
return {
list: [],
pageSize: 8,
total: 0,
pageNum: 0,
category: '100057'
}
},
mounted(){
this.getProductList();
},
methods: {
getProductList() {
this.$http.get('/products', {
params: {
pageSize: this.pageSize,
pageNum: this.pageNum,
category: this.category
}
}).then((res) => {
this.list = [res.data.data.records.slice(0, 4), res.data.data.records.slice(4, 8)];
this.total = res.data.data.total;
})
},
change(pageNum) {
this.pageNum = pageNum;
this.getProductList();
}
}
}
</script>
<style lang="scss">
@import './../assets/scss/config.scss';
@import './../assets/scss/mixin.scss';
.page{
.wrapper{
display:flex;
.list-box{
.pagination{
text-align:right;
}
.list{
@include flex();
width:986px;
margin-bottom:14px;
&:last-child{
margin-bottom:0;
}
.item{
width:236px;
height:302px;
background-color:$colorG;
text-align:center;
span{
display:inline-block;
width:67px;
height:24px;
font-size:14px;
line-height:24px;
color:$colorG;
}
.item-img{
img{
width:100%;
height:195px;
}
}
.item-info{
h3{
font-size:$fontK;
color:$colorB;
line-height:$fontK;
font-weight:bold;
}
p{
color:$colorD;
line-height:13px;
margin:6px auto 13px;
}
.price{
color:#F20A0A;
font-size:$fontJ;
font-weight:bold;
cursor:pointer;
&:after{
@include bgImg(22px,22px,'/imgs/cart.png');
content:' ';
margin-left:5px;
vertical-align: middle;
}
}
}
}
}
}
}
}
</style>
分页展示的核心逻辑在这个组件里面。
<el-pagination
class="pagination"
background
layout="prev, pager, next"
:pageSize="pageSize"
:total="total"
@current-change="change"
>
</el-pagination>
mounted(){
this.getProductList();
},
methods: {
getProductList() {
this.$http.get('/products', {
params: {
pageSize: this.pageSize,
pageNum: this.pageNum,
category: this.category
}
}).then((res) => {
this.list = [res.data.data.records.slice(0, 4), res.data.data.records.slice(4, 8)];
this.total = res.data.data.total;
})
},
change(pageNum) {
this.pageNum = pageNum;
this.getProductList();
}
}
在Vue的生命周期钩子函数mounted()
中调用getProductList()
,请求后端商品接口,商品接口会返回相应的商品数据,如一共有多少条符合要求的商品数据(默认搜索category: '100057'
,即商品类目ID是100057
的商品,其实就是手机),这样前端就知道商品数据可以分多少页了(符合要求的商品总数/pageSize
,默认pageSize: 8
),商品接口还会返回前端指定页的商品数据(默认pageNum: 0
),而当用户点击下一页、上一页或者指定页时,会触发change()
(@current-change="change"
),从而前端又会去请求商品接口获取商品分页数据(事件@current-change
、prev-click
、next-click
的回调参数是当前页,也就是说事件触发时,会将当前页传给触发函数,如change(pageNum)
函数中的pageNum
就是当前页;当事件prev-click
、next-click
触发时,事件@current-change
也会触发,应该很容易理解,所以为了简单,博主就只写事件@current-change
的触发函数)。
- Vue - Vue生命周期钩子
-
config.scss
:样式规范表,定义一些样式的规范,方便样式规范的统一和样式复用。
/*
样式规范表
*/
$min-width:1226px; //容器安全区域宽度
// 常规字体大小设置
$fontA: 80px; //产品站大标题
$fontB: 38px; //产品站标题
$fontC: 28px; //导航标题
$fontD: 26px; //产品站副标题
$fontE: 24px;
$fontF: 22px;
$fontG: 20px; //用在较为重要的文字、操作按钮
$fontH: 18px; //用于大多数文字
$fontI: 16px; //用于辅助性文字
$fontJ: 14px; //用于一般文字
$fontK: 12px; //系统默认大小
// 常规配色设置
$colorA: #FF6600 !default; //用于需要突出和强调的文字、按钮和icon
$colorB: #333333 !default; //用于较为重要的文字信息、内页标题等
$colorC: #666666 !default; //用于普通段落信息 引导词
$colorD: #999999 !default; //用于辅助、此要的文字信息、普通按钮的描边
$colorE: #cccccc !default; //用于特别弱的文字
$colorF: #d7d7d7 !default; //用于列表分割线、标签秒变
$colorG: #ffffff !default; //用于导航栏文字、按钮文字、白色背景
$colorH: #e5e5e5 !default; //用于上下模块分割线
$colorI: #000000 !default; //纯黑色背景,用于遮罩层
$colorJ: #F5F5F5 !default; //弹框标题背景色
$colorK: #FFFAF7 !default; //订单标题背景色
-
mixin.scss
:将公共的CSS提取出来,可以简化CSS的编写。
//flex布局复用
@mixin flex($hov:space-between,$col:center){
display:flex;
justify-content:$hov;
align-items:$col;
}
@mixin bgImg($w:0,$h:0,$img:'',$size:contain){
display:inline-block;
width:$w;
height:$h;
background:url($img) no-repeat center;
background-size:$size;
}
@mixin position($pos:absolute,$top:0,$left:0,$w:100%,$h:100%){
position:$pos;
top:$top;
left:$left;
width:$w;
height:$h;
}
@mixin positionImg($pos:absolute,$top:0,$right:0,$w:0,$h:0,$img:''){
position:$pos;
top:$top;
right:$right;
width:$w;
height:$h;
background:url($img) no-repeat center;
background-size:contain;
}
@mixin height($h:0,$lh:$h) {
height: $h;
line-height: $lh;
}
@mixin wH($w:0,$h:0) {
width:$w;
height: $h;
}
@mixin border($bw:1px,$bc:$colorF,$bs:solid) {
border: $bw $bs $bc;
}
购物车的图标:
前端也介绍到这里。
写博客是博主记录自己的学习过程,如果有错误,请指正,谢谢!