文章目录
前言
继上次写了springboot+mybatis+ajax实现注册功能后,这几天花了时间写了注册功能,这个功能不是用ajax异步实现的,而是控制层直接跳转页面,然后利用thymeleaf在前端展示, 这里我还是推荐用ajax,因为ajax实现的话,前端的一些效果不会被局限到。 因为我之前实现注册功能中很详细的写了用spring boot的整个流程,所以这一篇主要写pagehelper分页插件的使用还有前端thymeleaf要怎么处理等等一些细节,这里的一些配置都在注册功能那一篇的基础上加。
正文
业务场景
前端commons.html页面中标题栏有“专题”和“分类”(因为两者实现是一样的,所以我只写“专题”的实现),点击其下的标签,。
跳转到archive.html页面,该页面展示出分页的内容。
后端
pom.xml
这里说明一下,有的教程是用pagehelper的,两种其实本质都是一样的,写法配置可能不同,具体看mybatis使用pagehelper-spring-boot-starter与pagehelper的区别
<!--分页插件-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
application.yml
或者自己配置到application.properties中也一样。
#分页设置
pagehelper:
reasonable: true # reasonable:启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页。
support-methods-arguments: true
params: count=countSql
helper-dialect: mysql #数据库类型
额外话题,这里修改一下application.yml原来数据库的配置,因为我的数据库是放在centos虚拟机里面的,一开始查出来的时间相差8个小时,后面查了一下是我的虚拟机时区不是中国的(改虚拟机时区教程:mybatis查询mysql数据库出来的时间少8小时解决办法),后面改了时区后又相差14小时,后面查了说是mysql的时区不正确。所以下面的url加上了时区设定。
#因“CST”时区协商误解导致时间差了14或13小时 https://www.cnblogs.com/todayforever/p/11997424.html 与数据库连接时,定义时区,避免mybatis框架从mysql获取时区。在连接上加上 serverTimezone=GMT%2B8
url: jdbc:mysql://192.168.0.113:3306/videoweb?serverTimezone=GMT%2B8 #路径,这里用虚拟机的mysql
实体类video.java和User.java----映射VideoMapper.xml----VideoMapper接口
因为我这是个视频网站,数据库设计如下,所以需要查出视频的相关信息以及视频发布者的名字,所以要多表查询,mapper看起来可能复杂点。这里涉及mybatis多表关系就不细说了,具体自己百度,速成的话我推荐how2j.cn中mybatis的教程,里面就有多表关系快速使用教程。另外要说明一下,mapper.xml查询到的属性必须和实体类中的属性相对应,到时候前端才能取到。 举个例子我一开始video表是没有countcomments、countlikes、countlooks这三项的,实体类Video中也没有对应的属性,而是用mysql中count()函数进行复杂查询查出相关数据,后面发现前端没办法取到值,因为尽管查询语句查到了,但是mybatis不知道你的数据要存到实体类中的哪个值。
数据库
import java.util.Date;
import java.util.List;
public class Video {
private Integer vid; //视频id
private String vname; //视频名
private String vtag; //视频标签
// @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") //https://www.cnblogs.com/huanggy/p/9471827.html
// @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
private Date date; //上传日期
private String introduce; //视频介绍
private String url; //视频路径
private String imageurl; //视频封面路径
private Byte state; //视频状态,通过/审核
private String special; //专题
private Integer countcomments; //评论数
private Integer countlikes; //喜欢数
private Integer countlooks; //观看数
// 多对一,多个视频由一个用户上传
private User user;
// 一对多,一个视频有多个用户喜欢的记录
List<LikesKey> likesKeys;
// 一对多,一个视频多条评论
List<Comments> comments;
省略get和set方法....
}
import java.util.Date;
public class User { //用户
private Integer uid; //用户id
private String username; //用户名
private String password; //用户密码
private String email; //用户邮箱
private Date birthday; //用户出生日期
省略get和set方法....
}
<?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.liaojiexin.videoweb.mapper.VideoMapper" >
<resultMap id="BaseResultMap" type="com.liaojiexin.videoweb.entity.Video" >
<id column="vid" property="vid" jdbcType="INTEGER" />
<result column="vname" property="vname" jdbcType="VARCHAR" />
<result column="vtag" property="vtag" jdbcType="CHAR" />
<result column="date" property="date" jdbcType="TIMESTAMP" />
<!-- <result column="uid" property="uid" jdbcType="INTEGER" />-->
<result column="introduce" property="introduce" jdbcType="VARCHAR" />
<result column="url" property="url" jdbcType="VARCHAR" />
<result column="imageurl" property="imageurl" jdbcType="VARCHAR" />
<result column="state" property="state" jdbcType="TINYINT" />
<result column="special" property="special" jdbcType="VARCHAR" />
<result column="countcomments" property="countcomments" jdbcType="INTEGER" />
<result column="countlikes" property="countlikes" jdbcType="INTEGER" />
<result column="countlooks" property="countlooks" jdbcType="INTEGER" />
<!-- 多对一的关系 -->
<!-- column:指的是数据库队列名,property: 指的是属性名称, javaType:指的是属性的类型 -->
<association property="user" javaType="com.liaojiexin.videoweb.entity.User">
<id column="uid" property="uid" jdbcType="INTEGER" />
<result column="username" property="username" jdbcType="VARCHAR" />
<result column="password" property="password" jdbcType="VARCHAR" />
<result column="email" property="email" jdbcType="VARCHAR" />
<result column="birthday" property="birthday" jdbcType="DATE" />
</association>
<!-- 一对多的关系 -->
<!-- property: 指的是集合属性的值, ofType:指的是集合中元素的类型 -->
<collection property="likesKeys" ofType="com.liaojiexin.videoweb.entity.LikesKey">
<id column="uid" property="uid" jdbcType="INTEGER" />
<id column="vid" property="vid" jdbcType="INTEGER" />
</collection>
<!-- property: 指的是集合属性的值, ofType:指的是集合中元素的类型 -->
<collection property="comments" ofType="com.liaojiexin.videoweb.entity.Comments">
<id column="commenttime" property="commenttime" jdbcType="TIMESTAMP" />
<id column="uid" property="uid" jdbcType="INTEGER" />
<id column="vid" property="vid" jdbcType="INTEGER" />
<result column="comment" property="comment" jdbcType="VARCHAR" />
</collection>
</resultMap>
<sql id="Base_Column_List" >
vid, vname, vtag, date, uid, introduce, url, imageurl, state, special, countcomments,
countlikes, countlooks
</sql>
<!-- 首页查询专题,视频详细信息和用户名,评论数,喜欢数,观看数 -->
<select id="specialSelect" resultMap="BaseResultMap">
select video.*,username
from video,user
where special=#{special,jdbcType=VARCHAR} and video.uid=user.uid and video.state=1
order by video.date DESC
</select>
</mapper>
import java.util.List;
@Repository
@Mapper
public interface VideoMapper {
//首页查询专题,视频详细信息和用户名,评论数,喜欢数,观看数
List specialSelect(@Param("special") String special);
}
服务层ArchiveShowServiceImpl实现类和ArchiveShowService接口
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.liaojiexin.videoweb.mapper.VideoMapper;
import com.liaojiexin.videoweb.service.ArchiveShowService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ArchiveShowServiceImpl implements ArchiveShowService { //处理视频浏览页的服务
@Autowired //注入DAO
private VideoMapper videoMapper;
@Override //该标签会检查检查重写方法的正确性
public PageInfo specialSelect(String special,int pageNum, int pageSize){ //首页专题页显示,开启分页插件,放在查询语句上面 帮助生成分页语句
PageHelper.startPage(pageNum, pageSize);//底层实现原理采用改写语句 将下面的方法中的sql语句获取到然后做个拼接 limit
try {
List specialselect=videoMapper.specialSelect(special); //全部的视频信息
// 封装分页之后的数据 返回给客户端展示 PageInfo做了一些封装 作为一个类
PageInfo pageInfoSpecialSelect = new PageInfo(specialselect);
//所有分页属性都可以从pageInfoDemo拿到
return pageInfoSpecialSelect;
}finally {
PageHelper.clearPage(); //清理 ThreadLocal 存储的分页参数,保证线程安全
}
}
}
import com.github.pagehelper.PageInfo;
public interface ArchiveShowService { //视频浏览页的服务
PageInfo specialSelect(String special,int pageNum, int pageSize); //首页专题页显示
}
控制层SpecialController
这里把分页栏的数字变动处理也交由控制层处理。本来想写前端js来实现的,但是发现前端点击按钮后跳转请求页面刷新所以没法实现(所以说还是有ajax来实现比较好,这里如果用bootstrap自带的效果也能展现出来)
import com.github.pagehelper.PageInfo;
import com.liaojiexin.videoweb.service.ArchiveShowService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.ArrayList;
import java.util.List;
@Controller
public class SpecialController { //专题控制层
@Autowired
private ArchiveShowService archiveShowService;
@GetMapping(value = "/specialSelect/{special}")
private String specialShow(@PathVariable("special") String special,
@RequestParam(value = "pageNum", defaultValue = "1") int pageNum, //在参数里接受当前是第几页 pageNum,以及每页显示多少条数据 pageSize.默认值分别是1和10
@RequestParam(value = "pageSize", defaultValue = "10") int pageSize,
Model model)
{
PageInfo specialSelect=archiveShowService.specialSelect(special,pageNum,pageSize);
model.addAttribute("pages",specialSelect);
// 处理前端分页栏数字变动问题
List pagenums = new ArrayList();
if(pageNum>3&&specialSelect.getPages()>5) //当前端页数超过第三页时,并且查询到的总页数大于5
{
if(pageNum>=specialSelect.getPages()-2){ //specialSelect.getPages()总页数,如果前端页数大于等于总页数-2时
pagenums.add(specialSelect.getPages()-4);
pagenums.add(specialSelect.getPages()-3);
pagenums.add(specialSelect.getPages()-2);
pagenums.add(specialSelect.getPages()-1);
pagenums.add(specialSelect.getPages());
}
else{
pagenums.add(pageNum-2);
pagenums.add(pageNum-1);
pagenums.add(pageNum);
pagenums.add(pageNum+1);
pagenums.add(pageNum+2);
}
}else{ //前端页数没超过第三页时
if(specialSelect.getPages()<5) //如果总页数小于5
{
for(int i=0;i<specialSelect.getPages();i++)
pagenums.add(i+1);
}else{
pagenums.add("1");
pagenums.add("2");
pagenums.add("3");
pagenums.add("4");
pagenums.add("5");
}
}
model.addAttribute("pagenums",pagenums);
//跳到到personal.html页面,thymeleaf默认会拼串,classpath:/templates/xxx.html
return "archive"; //这里不可以写redirect:/archive,前端页面获取不到model,https://www.wandouip.com/t5i350864/
}
}
前端
这里前端模板用thymeleaf,还有bootstrap来实现样式。
commons.html
这里写专题的那部分,其他的省略,点击a标签连接后发出“/specialSelect/第四届NEW ERA青年电影季”的请求到控制层。
<li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-play-circle-o"></i> 专题</a>
<div class="dropdown-menu">
<div class="dropdown-inner">
<ul class="list-unstyled">
<li><a th:href="@{/specialSelect/第四届NEW ERA青年电影季}">第四届NEW ERA青年电影季</a></li>
<li><a th:href="@{/specialSelect/第91届奥斯卡入围动画短片大赏}">第91届奥斯卡入围动画短片大赏</a></li>
<li><a th:href="@{/specialSelect/2019戛纳广告节必看广告合集}">2019戛纳广告节必看广告合集</a></li>
<li><a th:href="@{/specialSelect/历届奥斯卡优秀短片合集}">历届奥斯卡优秀短片合集</a></li>
<li><a th:href="@{/specialSelect/2019年V视频年度精选短片回顾}">2019年V视频年度精选短片回顾</a></li>
</ul>
</div>
</div>
</li>
archive.html
控制层处理好数据后跳转到archive页面,然后用thymeleaf处理数据。
这里主要看两个模块,我在下面代码中也用分割线分割出来。
这里主要说thymeleaf中th:each的使用细节,我一开始看很多教程都没有细说,所以我尽管后端写好了,前端一直调用失败。
很多教程都是写th:each="page:${pages}"
,然后循环体内的标签属性写th:text="${page.xxx}"
或者在标签内容写[[${page.xxx}]]
,这种写法都不太正确,具体应该看你们代码是怎么写的,比较正确的方法如下:
用IDEA编译器debug一下你的控制层,如下图所示,你会发现正确的写法应该是th:each="page:${pages.list}"
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="">
<meta name="author" content="">
<title>浏览页</title>
<!-- Bootstrap Core CSS -->
<link rel="stylesheet" href="/css/bootstrap.min.css" type="text/css">
<!-- Custom CSS -->
<link rel="stylesheet" href="/css/style.css">
<!-- Owl Carousel Assets -->
<link href="/owl-carousel/owl.carousel.css" rel="stylesheet">
<link href="/owl-carousel/owl.theme.css" rel="stylesheet">
<!-- Custom Fonts -->
<link rel="stylesheet" href="/font-awesome-4.4.0/css/font-awesome.min.css" type="text/css">
<!-- jQuery and Modernizr-->
<script src="/js/jquery-2.1.1.js"></script>
<!-- Core JavaScript Files -->
<script src="/js/bootstrap.min.js"></script>
<script src="/js/archive.js"></script>
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="/js/html5shiv.js"></script>
<script src="/js/respond.min.js"></script>
<![endif]-->
</head>
<body>
<header th:replace="commons :: commonHeader"></header>
<!-- /Content -->
<div id="page-content" class="archive-page">
<div class="container">
<div class="row">
<!---------------------------------分页视频详细模块---------------------------------------->
<div id="main-content" class="col-md-6" style="border-bottom:0px;border-top: 0px;padding-bottom: 0px" th:each="page:${pages.list}">
<article style="height: 257px">
<a href="#"><h4 class="vid-name" style="overflow: hidden;text-overflow:ellipsis;white-space: nowrap;">[[${page.vname}]]</h4></a>
<div class="info">
<h5>上传人:<a>[[${page.user.username}]]</a></h5>
<!-- ↓↓↓ 格式化时间日期 -->
<span><i class="fa fa-calendar"></i>[[${#dates.format(page.date,"yyyy-MM-dd HH:mm:ss")}]]</span>
<span><i class="fa fa-comment"></i> [[${page.countcomments}]] 评论</span>
<span><i class="fa fa-heart"></i>[[${page.countlikes}]]</span>
<span><i class="fa fa-eye"></i>[[${page.countlooks}]]</span>
<ul class="list-inline"></ul>
</div>
<div class="wrap-vid">
<div class="zoom-container">
<div class="zoom-caption">
<span>[[${page.vtag}]]</span>
<a href="single.html">
<i class="fa fa-play-circle-o fa-5x" style="color: #fff"></i>
</a>
<p style="font-size: 17px;overflow: hidden;text-overflow:ellipsis;white-space: nowrap;" >[[${page.vname}]]</p>
</div>
<img src="/images/9.jpg" />
</div>
<!-- p标签超出省略 http://www.bubuko.com/infodetail-2738912.html -->
<p style="font-size:18px;display: -webkit-box; word-break:break-all;-webkit-box-orient: vertical;-webkit-line-clamp: 6;text-overflow:ellipsis;overflow: hidden;text-indent:2em" th:text="${page.introduce}"></p>
</div>
</article>
<br><div class="line" style="margin: 0"></div>
</div>
<!------------------------------------------------------------------------------------->
</div>
<div class="row">
<div id="main-content2" class="col-md-12">
<!---------------------------------分页栏模块-------------------------------------->
<span>相关记录视频[[${pages.total}]]条,共[[${pages.pages}]]页。</span>
<center>
<ul class="pagination">
<li><a id="firstpage" th:href="${#httpServletRequest.getRequestURL()} +'?pageNum=1'">首页</a></li>
</ul>
<ul class="pagination">
<li>
<a th:if="${pages.pageNum} > '1'" id="previouspage" th:href="${#httpServletRequest.getRequestURL()}+'?pageNum='+${pages.prePage}" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
</ul>
<!-- 当前页数 ,方便js调用 display:none;隐藏 -->
<span style="display:none;" id="pagenums">[[${pages.pageNum}]]</span>
<ul class="pagination" th:each="pagenum:${pagenums}">
<!-- ${#httpServletRequest.getRequestURL()}表示当前url,href中也可以不写,如下直接加参数 -->
<li><a th:class="'pagenum'+${pagenum}" th:href="'?pageNum='+${pagenum}">[[${pagenum}]]</a></li>
</ul>
<ul class="pagination">
<li>
<a th:if="${pages.pageNum} < ${pages.pages}" id="nextpage" th:href="${#httpServletRequest.getRequestURL()}+'?pageNum='+${pages.nextPage}" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
<ul class="pagination">
<li><a id="endpage" th:href="${#httpServletRequest.getRequestURL()}+'?pageNum='+${pages.pages}">末页</a></li>
</ul>
</center>
<!------------------------------------------------------------------------------------->
</div>
</div>
</div>
</div>
<footer th:replace="commons :: commonFooter"></footer>
</body>
</html>
archive.js
这里主要是让分页栏显示对应页数按钮的样式
$(document).ready(function () {
var pagenum='.pagenum'+$("#pagenums").text();
$(pagenum).css({"background-color":"red","color":"white"});
});
展示js的效果差别
结语
以上就是实现分页功能的教程,里面还有很多细节性问题和知识可以值得你们去思考和学习(像虚拟机时区问题、查询时间格式化问题、p标签内容字数超出规定的长度变省略号…等一系列问题),具体可以看我代码的注释。如果有什么问题欢迎留言互相谈论。
参考:
- SpringBoot+PageHelper+Bootstrap+Thymeleaf 实现分页功能
- SpringBoot2.0集成分页插件pagehelper-spring-boot-starter
- 结合后台pageInfo实现分页功能(这个是用ajax来处理的,前端js展示代码,如果用ajax的话可以参考)
- SpringBoot + thymeleaf 实现分页
业务问题扩展:我的项目里面有一个搜索功能,一开始我想要用同一个前端页面(即archive.html)来展示搜索结果,但是突然卡住了,因为我的搜索功能是一个form表单,发送请求到后端是不需要在路径中赋值的。如下代码
<form th:action="@{/checkSelect}" class="form-inline">
<input class="form-control required" type="" name="vname" id="search-form" placeholder="请输入要搜索的视频名称" required/>
<button class="btn" type="submit" style="color: black"><i class="fa fa-search"></i></button>
</form>
@Controller
public class CheckController { //搜索控制层
@Autowired
private ArchiveShowService archiveShowService;
@GetMapping(value = "/checkSelect/{vname}")
private String checkShow(@PathVariable("vname") String vname,
@RequestParam(value = "pageNum", defaultValue = "1") int pageNum, //在参数里接受当前是第几页 pageNum,以及每页显示多少条数据 pageSize.默认值分别是1和10
@RequestParam(value = "pageSize", defaultValue = "10") int pageSize,
Model model)
{
省略代码........
}
}
一开始我老是想在怎么把input的值赋值到url路径中,即:/checkSelect/xxx一样,但是form直接把input的值当成参数附在后面,即:/checkSelect?vame=xxx。这样的话,archive.html页面的分页栏就会报错,显示缺少vame参数。
想了各种方法,像js来赋值或者thymeleaf等等,弄了一晚上结果都不行,后面自己老老实实写了个新的前端页面来展示搜索效果,在修改这个这个新的页面分页栏路径时突然得到灵感。
我把控制层代码改成如下,把请求和参数vname改了,同时把vame赋值到model中,然后前端把分页栏的代码的路径改成如下来获取vame参数。
@Controller
public class CheckController { //搜索控制层
@Autowired
private ArchiveShowService archiveShowService;
@GetMapping(value = "/checkSelect")
private String checkShow(@RequestParam("vname") String vname,
@RequestParam(value = "pageNum", defaultValue = "1") int pageNum, //在参数里接受当前是第几页 pageNum,以及每页显示多少条数据 pageSize.默认值分别是1和10
@RequestParam(value = "pageSize", defaultValue = "10") int pageSize,
Model model)
{
model.addAttribute("vname",vname); //把vname传给分页栏
代码省略.........
}
}
<div id="main-content2" class="col-md-12">
<center>
<ul class="pagination">
<li><a id="firstpage" th:href="${#httpServletRequest.getRequestURL()} +'?pageNum=1&vname='+${vname} ">首页</a></li>
</ul>
<ul class="pagination">
<li>
<a th:if="${pages.pageNum} > '1'" id="previouspage" th:href="${#httpServletRequest.getRequestURL()}+'?pageNum='+${pages.prePage}+'&vname='+${vname} " aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
</ul>
<!-- 当前页数 ,方便js调用 display:none;隐藏 -->
<span style="display:none;" id="pagenums">[[${pages.pageNum}]]</span>
<ul class="pagination" th:each="pagenum:${pagenums}">
<!-- ${#httpServletRequest.getRequestURL()}表示当前url,href中也可以不写,如下直接加参数 -->
<li><a th:class="'pagenum'+${pagenum}" th:href="'?pageNum='+${pagenum}+'&vname='+${vname}">[[${pagenum}]]</a></li>
</ul>
<ul class="pagination">
<li>
<a th:if="${pages.pageNum} < ${pages.pages}" id="nextpage" th:href="${#httpServletRequest.getRequestURL()}+'?pageNum='+${pages.nextPage}+'&vname='+${vname}" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
<ul class="pagination">
<li><a id="endpage" th:href="${#httpServletRequest.getRequestURL()}+'?pageNum='+${pages.pages}+'&vname='+${vname}">末页</a></li>
</ul>
</center>
</div>
这样的话,“专题”和“分类”没有vname参数的话,就会显示null,不会影响功能实现。
而搜索功能也能完美实现