黑马旅游网(5):旅游线路分页展示
项目课程链接:https://www.bilibili.com/video/BV1CE411E7h4
完整课程连接:https://www.bilibili.com/video/BV1uJ411k7wy
1 功能描述
接上篇黑马旅游网(4):分类数据展示,本篇博客将分析和实现将具体某类旅游线路分成多个页面在前端网页进行展示。本案例预先提供了 国内游
分类的相关图片和文本数据,当功能正确实现后,单击分类栏中的 国内游
按钮,便可切换至对应的旅游线路展示界面。
2 功能分析
2.1 线路数据内容
该项目在预先提供的建表 sql 语句脚本中已经包含了将描述文字和图片路径添加到数据库中的语句。实践中只要将这些内容根据 Maven 的文件管理约定放在正确的位置即可正确查询。
旅游线路数据在数据库中的形式如下图所示
其中比较主要的字段是 cid
,它表示的是旅游线路的类别。对应 tab_category
表中的 cid
字段。由于数据集中只有 国内游
分类,因此表中此字段全部是 5
。这里也可以从后端回写的 json 数据中佐证,也是前端提交请求的重要依据:
同时,项目中也已经预先定义了相应的 Bean 封装类,用于接收从数据库中查询到的数据实例
/**
* 旅游线路商品实体类
*/
public class Route implements Serializable {
private int rid;//线路id,必输
private String rname;//线路名称,必输
private double price;//价格,必输
private String routeIntroduce;//线路介绍
private String rflag; //是否上架,必输,0代表没有上架,1代表是上架
private String rdate; //上架时间
private String isThemeTour;//是否主题旅游,必输,0代表不是,1代表是
private int count;//收藏数量
private int cid;//所属分类,必输
private String rimage;//缩略图
private int sid;//所属商家
private String sourceId;//抓取数据的来源id
private Category category;//所属分类
private Seller sellerf;//所属商家
private List<RouteImg> routeImgList;//商品详情图片列表
/*getter & setter 方法*/
}
2.2 功能设计思路
分页展示旅游线路的实质是后端根据前端提交的页码以及每页需要展示的线路数量作为 sql 查询的约束条件进行分页查表和结果回写。也就是说,前端每执行一次翻页动作,都会向后端提交一次获取目标页码线路数据的 GET 请求,而非一次性从后端查询所有的线路数据。只是这类请求传递的请求参数具备一定规律,后端只需通过利用请求参数的内在规律来动态地编写 sql 语句去数据库中做分页查询,进而向前端回写数据即可。
采用前后端分离的思路实现这个功能。
- 对于前端而言,主要需要做两件事:
- 一是预先定义好分页样式模板,并能够根据后端提供的内容进行动态展示。
- 二是定义分页样式,包括总页数、每页显示数、分页栏显示的最大页码数量(10页),当前页的特殊样式控制等。
- 此外,在提交请求参数和接收响应参数时要做好边界条件的处理(空参、非法取值范围等)。
- 对于后端而言,依旧按照三层架构的设计思路。
- 针对线路数据表
tab_route
专门定义一系列业务逻辑类和接口:RouteServlet、RouteService(Impl)、RouteDao(Impl) 等。 - 分析前端提交的请求参数,通过归纳一些公式,从而计算查表需要的参数,构造动态 sql 语句完成查询和回写。
- 此外,与前端类似,对于接收浏览器提交的请求参数要做一定的边界条件处理。
- 针对线路数据表
2.3 分页参数分析
分析浏览器的渲染内容,需要前后端相互配合来达到期望的效果。
2.3.1 前端
前端在提交请求时需要提供页码 currentPage
和期望展示的旅游线路数量 pageSize
。在接收响应数据时,除去旅游线路内容之外还需要后端提供总页数 totaoPage
和总条目数/旅游线路总数 totalCount
。
2.3.2 后端
根据前端需要的渲染内容,后端接收并解析出请求参数后,dao 层需要执行两种查询:一是查询当前分类的总记录数,二是查询指定页面中的旅游线路内容。
查询当前分类总记录数
从数据库中查询当前分类总记录数,需要传入分类id cid
即可,使用聚合查询 COUNT 得到总记录计数 totalCount
。进一步可以结合 pageSize
计算总页数 totalPage
:
totolPage = ⌈ totalCount pageSize ⌉ \text{totolPage} = \left \lceil \frac{\text{totalCount}}{\text{pageSize} } \right \rceil totolPage=⌈pageSizetotalCount⌉
查询指定页面的的旅游线路
从数据库中查询指定页面的总记录数,需要获知起始查询记录的位置 start
:
start = ( currentPage − 1 ) × pageSize \text{start} = (\text{currentPage} - 1) \times \text{pageSize} start=(currentPage−1)×pageSize
将 start
与 currentPage
作为 LIMIT 的参数即可实现分页查询。
3 代码实现
3.1 后端
3.1.1 Servlet
在 RouteServlet.java 中定义分页查询方法 pageQuery():
/**
* 分页查询方法
*/
public void pageQuery(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 1.接收客户端浏览器提交的请求参数
String cidStr = request.getParameter("cid"); // 类别id
String currentPageStr = request.getParameter("currentPage"); // 当前页数
String pageSizeStr = request.getParameter("pageSize"); // 每页显示条数
// 2.处理参数
int cid = this.parseInt(cidStr, 0);
int currentPage = this.parseInt(currentPageStr, 1);
int pageSize = this.parseInt(pageSizeStr, 5);
// 3.调用service查询PageBean对象
PageBean<Route> routePageBean = routeService.pageQuery(cid, currentPage, pageSize);
// 4.将PageBean对象序列化json并回写客户端浏览器
this.writeValue(routePageBean, response);
}
其中涉及到一个将字符串转为数字的转换方法定义在了父类 BaseServlet 中:
public abstract class BaseServlet extends HttpServlet {
/*其它方法定义*/
/**
* 字符串数字类型转换方法
* 将数字字符串转换为数字,例如:"5" -> 5
* @param numStr 数字字符串
* @return 整型数字
* @throws ClassCastException
* 类型转换异常,避免传入的字符串内容不是数字
*/
protected int parseInt(String numStr, int defaultValue) throws ClassCastException {
return (numStr != null && numStr.length() > 0 && !"null".equalsIgnoreCase(numStr) && !"NaN".equalsIgnoreCase(numStr))
? Integer.parseInt(numStr) : defaultValue; // 注意:浏览器提交的空值到后台会被识别为<字符串"null">
}
/*其它方法定义*/
}
3.1.2 Service
在 RouteServiceImpl.java 中定义供 RouteServlet 调用的 pageQuery() 方法
/**
* 根据旅游线路类别进行分页查询
* @param cid 类别id
* @param currentPage 当前页数
* @param pageSize 每页显示条数
*/
@Override
public PageBean<Route> pageQuery(int cid, int currentPage, int pageSize) {
// 初始化 PageBean 封装对象
PageBean<Route> routePageBean = new PageBean<>();
// 计算起始记录数start,查询总记录数totalCount,计算总页数totalPage
int start = (currentPage - 1) * pageSize;
int totalCount = routeDao.findTotalCount(cid);
int totalPage = (totalCount % pageSize == 0) ? (totalCount / pageSize) : (totalCount / pageSize + 1);
routePageBean.setCurrentPage(currentPage); // 设置当前页码
routePageBean.setPageSize(pageSize); // 设置每页显示条数
routePageBean.setTotalPage(totalPage); // 设置总页数
routePageBean.setTotalCount(totalCount); // 查询并设置总记录数
routePageBean.setList(routeDao.findByPage(cid, start, pageSize)); // 查询并设置当前页显示记录
return routePageBean;
}
3.1.3 Dao
RouteDaoImpl.java 中需要定义两种查询方法:
findTotalCount():
/**
* 根据cid查询总记录数
*/
@Override
public int findTotalCount(int cid) {
// 1.定义sql模板
String sql = "SELECT count(*) FROM tab_route WHERE 1 = 1 ";
StringBuilder stringBuilder = new StringBuilder(sql); // 字符串拼接
ArrayList<Object> paramList = new ArrayList<>(); // sql参数列表
// 2.判断查询参数是否有值
if (cid != 0) {
stringBuilder.append(" AND cid = ? ");
paramList.add(cid);
}
sql = stringBuilder.toString(); // 拼接sql语句
return template.queryForObject(sql, Integer.class, paramList.toArray());
}
findByPage():
/**
* 查询当前页的数据集合
* @param cid 旅游线路类别id
* @param start 起始条目
* @param pageSize 每页显示条数
* @return Route Bean 对象构成的List集合
*/
@Override
public List<Route> findByPage(int cid, int start, int pageSize) {
// 1.定义sql模板
String sql = "SELECT * FROM tab_route WHERE 1 = 1";
StringBuilder stringBuilder = new StringBuilder(sql); // 字符串拼接
ArrayList<Object> paramList = new ArrayList<>(); // sql参数列表
// 2.判断查询参数是否有值
if (cid != 0) {
stringBuilder.append(" AND cid = ? ");
paramList.add(cid);
}
// 3.补充分页条件
stringBuilder.append(" LIMIT ? , ? ");
paramList.add(start);
paramList.add(pageSize);
sql = stringBuilder.toString(); // 拼接sql语句
return template.query(sql, new BeanPropertyRowMapper<>(Route.class), paramList.toArray());
}
3.1.4 route/pageQuery 程序的响应内容
3.2 前端
3.2.1 请求提交
请求提交体现在首页跳转和分页跳转,这里先给出首页跳转的代码,分页跳转在 3.2.3 小节中给出。
header.html:
/**
* 查询分类数据信息
*/
$.get("category/findAll", {}, function (data) {
// data: [{cid: 1, cname: 国内游}, {CategoryBean}, {CategoryBean}]
let lis = '<li class="nav-active"><a href="index.html">首页</a></li>';
// 遍历data数组,拼接字符串(<li>...</li>),`国内游`的链接跳转是循环中的一项(当 i = 5 时)
for (let i = 0; i < data.length; i ++) {
lis += '<li><a href="route_list.html?cid=' + data[i]["cid"] + '">' + data[i]["cname"] + '</a></li>';
}
// 拼接收藏排行榜<li><a href="favoriterank.html">收藏排行榜</a></li>
lis += '<li><a href="favoriterank.html">收藏排行榜</a></li>';
// 将lis字符串设置到ul的html内容中
$("#category").html(lis);
});
3.2.2 旅游线路内容展示
route_list.html:
// 2.列表数据展示
let routeLis = '';
for(let i = 0; i < pageBean.list.length; i ++) {
// 获取一个 Route Bean 对象
let route = pageBean.list[i];
let li = '<li>\n' +
' <div class="img"><img src="'+ route["rimage"] +'" style="width: 299px"></div>\n' +
' <div class="text1">\n' +
' <p>'+ route["rname"] +'</p>\n' +
' <br/>\n' +
' <p>'+ route["routeIntroduce"] +'</p>\n' +
' </div>\n' +
' <div class="price">\n' +
' <p class="price_num">\n' +
' <span>¥</span>\n' +
' <span>'+ route["price"] +'</span>\n' +
' <span>起</span>\n' +
' </p>\n' +
' <p><a href="route_detail.html?rid='+ route.rid +'">查看详情</a></p>\n' +
' </div>\n' +
' </li>';
routeLis += li;
}
$("#route").html(routeLis);
3.2.3 分页栏展示
分页查询的请求定义在动态H5字符串中。
route_list.html:
let pageLis = "";
// let href = '"localhost/travel/route_list.html?cid="''';
let firstPage = '<li οnclick="load('+ cid +','+ 1 + ',\''+ rname +'\')"><a href="route_list.html?cid='+ cid +'">首页</a></li>';
let previousPageNum = pageBean.currentPage-1 < 1 ? 1 : pageBean.currentPage-1; // 计算上一页页码
let previousPage = '<li οnclick="load('+ cid +','+ previousPageNum +',\''+ rname +'\')" class="threeword">' +
'<a href="route_list.html?cid='+ cid +'¤tPage='+ previousPageNum +'">上一页</a></li>';
pageLis += firstPage + previousPage;
// 1.2 展示分页页码
/* 显示部分页面
1.一共展示10个页码,能够达到前5后4的效果
2.如果前面不够5个,后面补齐10个
3.如果后面不足4个,前面补齐10个
*/
// 定义开始位置 begin,结束位置 end
let begin, end;
// 1.要显示10个页码
if (pageBean["totalPage"] < 10) { // 总页码不够10页
begin = 1;
end = pageBean["totalPage"];
} else { // 总页码超过10页
begin = currentPage - 5;
end = currentPage + 4;
// 2.如果前面不够5个,后面补齐10个
if (begin < 1) {
begin = 1;
end = begin + 9;
}
// 3.如果后面不足4个,前面补齐10个
if (end > pageBean["totalPage"]) {
end = pageBean["totalPage"];
begin = end - 9;
}
}
for (let i = begin; i <= end; i++) {
let li;
if (pageBean["currentPage"] === i) {
// 对当前页码添加特殊的CSS样式
li = '<li class="curPage" οnclick="load('+ cid +','+ i +',\''+ rname +'\')">' +
'<a href="route_list.html?cid='+ cid +'¤tPage='+ i +'">'+ i +'</a></li>';
} else {
li = '<li οnclick="load('+ cid +','+ i +',\''+ rname +'\')">' +
'<a href="route_list.html?cid='+ cid +'¤tPage='+ i +'">'+ i +'</a></li>';
}
pageLis += li;
}
let nextPageNum = pageBean.currentPage+1 > pageBean["totalPage"] ? pageBean["totalPage"] : pageBean.currentPage+1; // 计算下一页页码
let nextPage = '<li οnclick="load('+ cid +','+ nextPageNum +',\''+ rname +'\')" class="threeword">' +
'<a href="route_list.html?cid='+ cid +'¤tPage='+ nextPageNum +'">下一页</a></li>';
let lastPage = '<li οnclick="load('+ cid +','+ pageBean["totalPage"] +',\''+ rname +'\')" class="threeword">' +
'<a href="route_list.html?cid='+ cid +'¤tPage='+ pageBean["totalPage"] +'">末页</a></li>';
pageLis += nextPage + lastPage;
// 将lis内容设置到ul中
$("#ulPageNum").html(pageLis);
部分代码并未完全展示,完整代码可以参考我的 GitHub 仓库