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

前端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的功能和特性:

项目代码结构:
在这里插入图片描述

  • 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 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.jsondependencies里面。

对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-changeprev-clicknext-click的回调参数是当前页,也就是说事件触发时,会将当前页传给触发函数,如change(pageNum)函数中的pageNum就是当前页;当事件prev-clicknext-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;
}

购物车的图标:
在这里插入图片描述
前端也介绍到这里。

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

相关推荐
©️2020 CSDN 皮肤主题: 点我我会动 设计师:白松林 返回首页