前端Vue+ElementUI的Pagination分页组件实现分页展示 & 后端Spring Boot +Mybatis Plus实现分页接口

很久没有更新博客了,主要原因是博主一直在补充自己,发现自己还有很多地方不足,等博主补充好了,再来相互探讨技术。

主要是看到评论区的小伙伴问分页该怎么实现,博主就花了几个小时去实现这个小栗子。

演示

分页测试

数据库

有数据才能进行展示,为了简单,就一个​​product​​表。

前端Vue+ElementUI的Pagination分页组件实现分页展示 & 后端Spring Boot +Mybatis Plus实现分页接口_分页


博主去年爬了一些商品数据,用于博主的本科毕业设计,这个​​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​​条商品数据,毕竟博主爬了几天,当时写的爬虫现在也被反爬了。

前端Vue+ElementUI的Pagination分页组件实现分页展示 & 后端Spring Boot +Mybatis Plus实现分页接口_分页_02

后端

先了解一下Mybatis Plus的功能和特性:

  • ​​MyBatis Plus汇总​​

项目代码结构:

前端Vue+ElementUI的Pagination分页组件实现分页展示 & 后端Spring Boot +Mybatis Plus实现分页接口_java_03

  • ​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​​)。

项目结构:

前端Vue+ElementUI的Pagination分页组件实现分页展示 & 后端Spring Boot +Mybatis Plus实现分页接口_java_04

创建项目后,再下载一些依赖包:

"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>

前端Vue+ElementUI的Pagination分页组件实现分页展示 & 后端Spring Boot +Mybatis Plus实现分页接口_分页_05

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+ElementUI的Pagination分页组件实现分页展示 & 后端Spring Boot +Mybatis Plus实现分页接口_java_06

  • ​​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;
}

购物车的图标:

前端Vue+ElementUI的Pagination分页组件实现分页展示 & 后端Spring Boot +Mybatis Plus实现分页接口_spring_07


前端也介绍到这里。

写博客是博主记录自己的学习过程,如果有错误,请指正,谢谢!