Springboot + Bootstrap + Thymeleaf 分页

Springboot + Bootstrap + Thymeleaf 分页

前言

这里需要使用到的组件有:

  • Springboot 2.7.x
  • MySQL 5.7.x
  • Mybatis Plus 3.5.2
  • Bootstrap 4.5.x
  • Thymeleaf

添加依赖和配置 application.properties

依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

配置 application.properties

# Thymeleaf Configuration
# 关闭缓存
spring.thymeleaf.cache=false
# 字符编码
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.mode=HTML
# 模板文件位置
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

# MySQL Configuration
spring.datasource.url=jdbc:mysql://localhost:3306/dong?serverTimezone=UTC&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=root#admin

# HikariCP Configuration
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=15
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.pool-name=DatebookHikariCP
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1

# Tomcat Port
server.port=18443
server.address=localhost

Model,View,Controller layers

在数据层,先创建一个简单的实体类Post,包含titlebody字段

import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

@Data
public class Post {
    @TableId
    private Long id;
    private String title;
    private String body;
}

在数据访问层(PostDao),直接使用 Mybatisplus提供的 API

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.syuez.dong.entity.Post;

public interface PostDAO extends BaseMapper<Post> {
}

在服务层,PostService类负责查询数据,并将结果与Paged实例打包

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.syuez.dong.entity.Paged;
import com.syuez.dong.entity.Paging;
import com.syuez.dong.entity.Post;
import com.syuez.dong.mapper.PostDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PostService {

    private final PostDAO postDAO;

    @Autowired
    public PostService(PostDAO postDAO) {
        this.postDAO = postDAO;
    }

    public Paged<Post> getPage(int pageNumber, int size) {
        Page<Post> page = new Page<>(pageNumber, size);
        postDAO.selectPage(page, null);
        /*
          总页数 = 总条目数÷每页显示目数,如果整除则取商,如果有余数则取(商+1)
         */
        int totalPages = (int) page.getTotal() % size == 0 ? (int) page.getTotal() / size : (int) page.getTotal() / size + 1;
        return new Paged<>(page, Paging.of(totalPages, pageNumber,size));
    }
}

PagedPageItemPageItemTypePaging类是用于准备分页组件的工具类

Paged类结构

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Getter;
public class Paged<T> {
    @Getter
    private final Page<T> page;    
    @Getter
    private final Paging paging;

    public Paged(Page<T> page, Paging paging) {
        this.page = page;
        this.paging = paging;
    }
}

PageItem类结构

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class PageItem {

    private PageItemType pageItemType;
    /**
     * 当前索引
     */
    private int index;
    /**
     * 是否激活
     */
    private boolean active;
}

PageItemType结构

/**
 * 分页类型
 */
public enum PageItemType {
    /**
     * 省略号
     */
    DOTS,
    /**
     * 页面
     */
    PAGE
}

Paging类负责计算如何在分页组件中显示页面和...

import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;
public class Paging {

    private static final int PAGINATION_STEP = 3;
    /**
     * 是否存在下一页
     */
    @Setter
    @Getter
    private boolean nextEnabled;
    /**
     * 是否存在上一页
     */
    @Setter
    @Getter
    private boolean prevEnabled;
    /**
     * 每页显示多少数据
     */
    @Setter
    @Getter
    private int pageSize;
    /**
     * 当前是第几页
     */
    @Setter
    @Getter
    private int pageNumber;
    /**
     * 分页列表
     */    
    @Getter
    private List<PageItem> items = new ArrayList<>();

    public void addPageItems(int from, int to, int pageNumber) {
        for (int i = from; i < to; i++) {
            items.add(PageItem.builder()
                    .active(pageNumber !=i)
                    .index(i)
                    .pageItemType(PageItemType.PAGE)
                    .build());
        }
    }

    public void lastPage(int pageSize) {
        items.add(PageItem.builder()
                .active(false)
                .pageItemType(PageItemType.DOTS)
                .build());

        items.add(PageItem.builder()
                .active(true)
                .index(pageSize)
                .pageItemType(PageItemType.PAGE)
                .build());
    }

    public void firstPage(int pageNumber) {
        items.add(PageItem.builder()
                .active(pageNumber != 1)
                .index(1)
                .pageItemType(PageItemType.PAGE)
                .build());

        items.add(PageItem.builder()
                .active(false)
                .pageItemType(PageItemType.DOTS)
                .build());
    }

    public static Paging of(int totalPages, int pageNumber, int pageSize) {
        Paging paging = new Paging();
        // 设置每页显示条目
        paging.setPageSize(pageSize);
        // 当前页不是总页数时,总有下一页
        paging.setNextEnabled(pageNumber != totalPages);
        // 当前页不是第一页时,总有上一页
        paging.setPrevEnabled(pageNumber != 1);
        // 设置当前页
        paging.setPageNumber(pageNumber);
        // 分页逻辑
        if (totalPages < PAGINATION_STEP * 2 + 6) {
            paging.addPageItems(1, totalPages + 1, pageNumber);
        } else if (pageNumber < PAGINATION_STEP * 2 + 1) {
            paging.addPageItems(1, PAGINATION_STEP * 2 + 4, pageNumber);
            paging.lastPage(totalPages);

        } else if (pageNumber > totalPages - PAGINATION_STEP * 2) {
            paging.firstPage(pageNumber);
            paging.addPageItems(totalPages - PAGINATION_STEP * 2 - 2, totalPages + 1, pageNumber);

        } else {
            paging.firstPage(pageNumber);
            paging.addPageItems(pageNumber - PAGINATION_STEP, pageNumber + PAGINATION_STEP + 1, pageNumber);
            paging.lastPage(totalPages);
        }

        return paging;
    }

}

GET 请求是由PostController类处理的。为了改变当前页面和页面显示的条目数量,我们需要添加两个请求参数:

  • pageNumber - 当前的页面(默认是第一页)
  • size - 每页显示条目数(默认是5行)
import com.syuez.dong.service.PostService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.ui.Model;

@Controller
@RequestMapping("/")
public class PostController {
    private final PostService postService;

    @Autowired
    public PostController(PostService postService) {
        this.postService = postService;
    }

    @GetMapping
    public String posts(@RequestParam(value = "pageNumber", required = false, defaultValue = "1") int pageNumber,
                        @RequestParam(value = "size", required = false, defaultValue = "5") int size, Model model) {
        model.addAttribute("posts", postService.getPage(pageNumber, size));
        return "posts";
    }
}

配置 MybatisPlush 的分页

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.syuez.dong.mapper")
public class MybatisPlusConfig {
    /**
     * 新的分页插件,分页插件,如果你不配置,分页插件将不生效,指定数据库方言为 MYSQL
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        // 单页分页条数限制
        pageInterceptor.setMaxLimit(20L);
        interceptor.addInnerInterceptor(pageInterceptor);
        return interceptor;
    }
}

Thymeleaf 模板

posts.html源码:

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
  <!-- Required meta tags -->
  <meta http-equiv="Content-Type" content="text/html" charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <!-- Bootstrap CSS -->
  <link type="text/css" th:href="@{css/bootstrap4/bootstrap.min.css}" rel="stylesheet">
  <title>狗屁不通文章</title>
</head>
<body>
<div class="container-fluid">
  <!-- Navigation -->
  <nav class="navbar navbar-expand-lg navbar-dark bg-dark static-top">
    <div class="container">
      <a class="navbar-brand" href="/">Thymeleaf - Bootstrap Pagination</a>
      <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive"
              aria-controls="navbarResponsive"
              aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarResponsive">
        <ul class="navbar-nav ml-auto">
          <li class="nav-item active">
            <a class="nav-link" href="#">Home
              <span class="sr-only">(current)</span>
            </a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="#">About</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="#">Services</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="#">Contact</a>
          </li>
        </ul>
      </div>
    </div>
  </nav>

  <div class="container">
    <div class="row">
      <div class="col-lg-10 mt-5 mb-5">
        <table id="posts" class="table table-bordered table-responsive-sm">
          <thead>
          <tr>
            <th>ID</th>
            <th>Title</th>
            <th>Body</th>
          </tr>
          </thead>
          <tbody>
          <tr th:each="post : ${posts.page.getRecords()}">
            <td th:text="${post.id}">id</td>
            <td th:text="${post.title}">title</td>
            <td th:text="${post.body}">body</td>
          </tr>
          </tbody>
        </table>

        <nav aria-label="Page navigation" class="paging">
          <ul class="pagination" th:if="${posts.page.getTotal() > 1}">
            <li class="page-item" th:classappend="${!posts.paging.isPrevEnabled()? 'disabled' : ''}">
              <a class="page-link" th:href="@{'/?pageNumber=' + ${posts.paging.pageNumber - 1}}"
                 tabindex="-1">上一页</a>
            </li>
            <th:block th:each="item : ${posts.paging.getItems()}">
              <li class="page-item" th:classappend="${item.index == posts.paging.pageNumber? 'active' : ''}"
                  th:if="${item.pageItemType.name() == 'PAGE'}">
                <a class="page-link" th:href="@{'/?pageNumber=' + ${item.index}}"
                   th:text="${item.index}"></a>
              </li>
              <li class="page-item disabled" th:if="${item.pageItemType.name() == 'DOTS'}">
                <a class="page-link" href="#">...</a>
              </li>
            </th:block>
            <li class="page-item" th:classappend="${!posts.paging.isNextEnabled()? 'disabled' : ''}">
              <a class="page-link" th:href="@{'/?pageNumber=' + ${posts.paging.pageNumber + 1}}">下一页</a>
            </li>
          </ul>
        </nav>

      </div>
    </div>
  </div>
</div>
<!-- jQuery first, then Popper.js and Bootstrap JS -->
<script type="text/javascript" th:src="@{js/jquery/jquery-3.6.0.min.js}" ></script>
<script type="text/javascript" th:src="@{js/bootstrapjs/bootstrap.bundle.min.js}"></script>
</body>
</html>

效果

请访问 https://dong.syuez.com 查看效果


本文转载自:Spring Boot + Bootstrap + Thymeleaf Pagination (JPA, Liquibase, H2) | FrontBackend 代码有略微修改。