当前位置:网站首页>第2章 前台数据展现
第2章 前台数据展现
2022-07-27 08:17:00 【沉睡羊sleepysheep】
目录
2-9 【选看】解决BUG:LocalDateTime异常的出现与解决方法
2-10 【同2-9】解决Bug:LocalDateTime异常的出现与解决方法
2-1 案例分析与数据库建表



导入一个sql



来看一下表结构和数据
book

member

evaluation


category


member-read-state


2-2 Vant使用入门



选择基于vue3的版本

日常开发的场景都给进行了封装


![]()

开发第一个页面

assets包含了 未来我们要使用到的各种各样js组件 复制粘贴到webapp目录下

创建vant.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vant演示页面</title>
<!--基于设备逻辑分辨率进行页面开发,禁止页面缩放-->
<meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0,user-scalable=no">
//引入css和js
<link rel="stylesheet" href="/assets/vant/index.css">
//引入vue3的js
<script src="/assets/vue/vue.global.js"></script>
//引入vant的js
<script src="/assets/vant/vant.min.js"></script>
<style>
.my-swipe .van-swipe-item {
color: #fff;
font-size: 20px;
line-height: 150px;
text-align: center;
background-color: #39a9ed;
}
</style>
</head>
<body>
//基于div容器来进行
<div id="app">
//swipe轮播组件
<van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
<van-swipe-item>1</van-swipe-item>
<van-swipe-item>2</van-swipe-item>
<van-swipe-item>3</van-swipe-item>
<van-swipe-item>4</van-swipe-item>
</van-swipe>
</div>
//完成当前页面应用的初始化工作
<script>
const main = {
data(){
return {}
}
}
//对当前的vue以及vant初始化
const app = Vue.createApp(main);
//安装vant
app.use(vant);
//延迟加载
app.use(vant.Lazyload);
//对id为app的div进行渲染工作
app.mount("#app");
</script>
</body>
</html>什么是逻辑分辨率?

应用程序完全按照物理分辨率来开发的话,对每个开发者来说就是一场噩梦,开发者要在无数种可能会出现的分辨率场景下来进行适配,防止同样尺寸的物件在不同设备上显示忽大忽小导致一些显示上的问题

虚拟的分辨率数值 在一个相对稳定的区间内进行微调 在不同设备上也不会也有太大的比例上的变化一定程度上简化了我们的开发
测试运行
爆出类不存在异常

![]()
没有发布新依赖

再次运行

访问页面
![]()

![]()

就出现了轮播组件
2-3 实现CategoryService分类数据查询

回到数据库

本节来完成这个数据表的查询工作
创建实体类

//与那张表进行映射
@TableName("category")
public class Category {
//主键生成的类型为
@TableId(type = IdType.AUTO)
private Long categoryId;
private String categoryName;
@Override
public String toString() {
return "Category{" +
"categoryId=" + categoryId +
", categoryName='" + categoryName + '\'' +
'}';
}
public Long getCategoryId() {
return categoryId;
}
public void setCategoryId(Long categoryId) {
this.categoryId = categoryId;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
}
创建接口

public interface CategoryMapper extends BaseMapper<Category> {
}
<?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.imooc.reader.mapper.CategoryMapper"> </mapper>

public interface CategoryService {
public List<Category> selectAll();
}
创建实现类

//说明是service类在ioc容器初始化的时候对其进行实例化
@Service
public class CategoryServiceImpl implements CategoryService {
//对categoryMapper进行注入
@Resource
private CategoryMapper categoryMapper;
@Override
public List<Category> selectAll() {
QueryWrapper wrapper = new QueryWrapper();
//升序排列
wrapper.orderByAsc("category_id");
return categoryMapper.selectList(wrapper);
}
}
为什么这里又是接口又是实现类呢?这个借口是为了以后有程序的扩展性需要,利用接口可以赋予程序更多的可扩展性 使用了springioc容器以后我们的接口和具体的实现类可以不进行强制的绑定
创建测试类

//在执行测试用例前先对ioc容器进行初始化
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class CategoryServiceTest {
//进行注入
@Resource
private CategoryService categoryService;
@Test
public void selectAll() {
List<Category> categories = categoryService.selectAll();
for(Category c:categories){
System.out.println(c);
}
}
}运行

回到CategoryService配置声明式事务
@Service
@Transactional(propagation = Propagation.NOT_SUPPORTED,readOnly = true)
public class CategoryServiceImpl implements CategoryService {
@Resource
private CategoryMapper categoryMapper;
@Override
public List<Category> selectAll() {
QueryWrapper wrapper = new QueryWrapper();
wrapper.orderByAsc("category_id");
return categoryMapper.selectList(wrapper);
}
}
未来 其他的方法有需要使用事务的时候,在方法的上方进行事务的开启就可以了
2-4 实现加载图书分类功能

额外新增几个文件,粘贴到webapp


打开index.html文件 提供了一系列的原型界面,所谓原型界面就是指包含了页面基本的逻辑以及完整的演示数据,然后在静态页面的基础上我们将它动态化和后端代码进行通信,这样做的好处有两个第一个,在前台页面不是我们学习的主要方向,我们学习的是后端技术,把重心放在和后端交互上。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0,user-scalable=no">
<title>慕课书评网</title>
<!-- 引入样式文件 -->
<link rel="stylesheet" href="/assets/vant/index.css"/>
<!-- 引入 Vue 和 Vant 的 JS 文件 -->
<script src="/assets/vue/vue.global.js"></script>
<script src="/assets/vant/vant.min.js"></script>
<script src="/assets/axios/axios.js"></script>
<style>
.van-card {
height: 160px
}
.van-card__thumb {
width: 110px
}
</style>
</head>
<body>
<div id="app">
<!--导航栏部分,显示慕课网Logo和登录按钮-->
<van-nav-bar @click-right="clickRight">
<template #left>
<a href="/" style="padding-top: 10px">
<img src="https://m.imooc.com/static/wap/static/common/img/logo2.png" style="width: 80px">
</a>
</template>
<template #right>
<!-- 根据会员登录状态决定显示昵称还是登录按钮 -->
<template v-if="state.isLogin">
<img src="/images/user_icon.png" style="height:30px">{
{member.nickname}}
</template>
<template v-if="!state.isLogin">
<img src="/images/user_icon.png" style="height: 30px">
登录
</template>
</template>
</van-nav-bar>
<van-row>
<!-- 分类筛选下拉框 -->
<van-col span="12">
<van-dropdown-menu>
<van-dropdown-item @change="changeCategory()" v-model="state.category" :options="categoryOptions"/>
</van-dropdown-menu>
</van-col>
<!-- 结果排序下拉框 -->
<van-col span="12">
<van-dropdown-menu>
<van-dropdown-item @change="changeOrder()" v-model="state.order" :options="orderOptions"/>
</van-dropdown-menu>
</van-col>
</van-row>
<!-- 图书列表,遍历加载bookList数据 -->
<template v-for="(item,index) in bookList">
<van-card @click="showDetail(index)">
<template #title>
<div style="font-size: 16px;font-weight: bold">{
{item.bookName}}</div>
<div style="margin-top:5px;background: #eeeeee;padding: 5px">{
{item.author}}</div>
<div style="font-size: 16px;margin-top:5px;">{
{item.subTitle}}</div>
<span style="line-height: 20px;margin-right: 10px">{
{item.evaluationScore}}分 {
{item.evaluationQuantity}}人已评</span>
<van-rate v-model="item.evaluationScore" color="#ffd21e" void-icon="star" readonly
allow-half></van-rate>
</template>
<template #thumb>
<img :src="item.cover" style="width:102px;height:136px;">
</template>
</van-card>
</template>
<!-- 当前如果不是最后一页,显示"加载更多"按钮 -->
<van-button v-show="!state.isLastPage" plain hairline type="primary" block style="margin-bottom:50px"
@click="loadMore">点击加载更多...
</van-button>
<!-- 当前如果是最后一页,则提示"没有其他数据" -->
<van-button v-show="state.isLastPage" disabled hairline type="default" block style="margin-bottom:50px">没有其他数据了
</van-button>
</div>
<script>
const main = {
data() { //初始数据
return {
bookList: [{ //图书数据
"bookId": 5,
"bookName": "从 0 开始学爬虫",
"subTitle": "零基础开始到大规模爬虫实战",
"author": "梁睿坤 · 19年资深架构师",
"cover": "/images/5ce256ea00014bc903600480.jpg",
"description": "...",
"categoryId": 2,
"evaluationScore": 4.9,
"evaluationQuantity": 15
}, {
"bookId": 25,
"bookName": "网络协议那些事儿",
"subTitle": "前后端通用系列课",
"author": "Oscar · 一线大厂高级软件工程师",
"cover": "/images/5da923d60001a92f05400720.jpg",
"description": "...",
"categoryId": 2,
"evaluationScore": 4.7,
"evaluationQuantity": 15
}, {
"bookId": 1,
"bookName": "教你用 Python 进阶量化交易",
"subTitle": "你的量化交易开发第一课",
"author": "袁霄 · 全栈工程师",
"cover": "/images/5c247b0b0001a0a903600480.jpg",
"description": "...",
"categoryId": 1,
"evaluationScore": 4.6,
"evaluationQuantity": 13
}] //当前要显示的书籍内容
, state: {
category: -1, //技术分类,默认分类选中"所有类别"
order: "quantity", //排序,默认按热度排序
page: 1, //查询页号
isLastPage: false,//是否为最后一页
isLogin: false,//当前页面状态是否已登录
member: {}//当前登录的会员数据
}
, categoryOptions: [
{text: "所有类别", value: -1}
]
, orderOptions: [
{text: "按热度排序", value: "quantity"},
{text: "按分数排序", value: "score"}
]
}
}
, methods: {
/*
* onchange 当页面状态发生变更后,Ajax重新查询图书数据
* isFlush 参数说明是否清空已有图书列表
*/
onchange: function (isFlush) {
},
//点击"加载更多"按钮时,向服务器查询下一页数据
loadMore: function () {
//如果当前不是最后一页,则当前页号+1并向服务器发起请求查询下一页数据
if (this.state.isLastPage == false) {
this.state.page = this.state.page + 1;
this.onchange();
}
}
//当更改"分类"下拉选项后,清空原有数据进行查询
, changeCategory: function () {
//页号重置为1
this.state.page = 1;
//重新查询图书数据
this.onchange(true);
}
//当更改"排序"下拉选项后,清空原有数据进行查询
, changeOrder: function () {
//页号重置为1
this.state.page = 1;
//重新查询图书数据
this.onchange(true);
}
//点击右上角"登录"按钮后,跳转至登录页
, clickRight: function () {
if (!this.state.isLogin) {
window.location.href = "/login.html";
}
}
//点击具体的图书专栏后,跳转到详情页面
, showDetail: function (index) {
const book = this.bookList[index];
window.location.href = "/detail.html?bid=" + book.bookId;
}
}
, mounted() {
//页面初始化时执行的操作
}
};
const app = Vue.createApp(main);
app.use(vant);
app.use(vant.Lazyload);
app.mount("#app");
</script>
</body>
</html>打开静态显示页面


新建控制器,查询分类数据

//说明当前所有的方法返回的都是json字符串
@RestController
//说明当前控制器映射的前缀是什么?
@RequestMapping("/api/category")
public class CategoryController {
//注入service
@Resource
private CategoryService categoryService;
@GetMapping("/list")
public Object list(){
//code = "0"
categoryService.selectAll();
}
}当前方法为什么要返回Object进行站位呢? 从服务器返回的json数据应该有一个标准结构,之前约定的标准结构是code编码如果=0代表服务器处理成功,如果不为0则代表服务器处理失败,关于编码的封装之前开发过一个工具类
增加工具包
![]()
创建工具类

public class ResponseUtils {
private String code;
private String message;
private Map data = new LinkedHashMap<>();
//将各种不同的场景来进行封装
public ResponseUtils(){
this.code = "0";
this.message = "success";
}
//处理失败时就传入对应的消息
public ResponseUtils(String code , String message){
this.code = code;
this.message = message;
}
//向data这个map中进行赋值的
public ResponseUtils put(String key , Object value){
this.data.put(key, value);
//连续调用putput方法一次性完成对数据的赋值
return this;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Map getData() {
return data;
}
public void setData(Map data) {
this.data = data;
}
} 在
进行相应的变更
@RestController
@RequestMapping("/api/category")
public class CategoryController {
@Resource
private CategoryService categoryService;
@GetMapping("/list")
public ResponseUtils list(){
//code = "0"
//处理成功进行一次实例化
ResponseUtils resp = null;
try {
//用list来进行接收
List<Category> categories = categoryService.selectAll();
//将list随着响应进行返回
resp = new ResponseUtils().put("list",categories);
}catch (Exception e){
e.printStackTrace();
//处理失败返回相应的错误信息
resp = new ResponseUtils(e.getClass().getSimpleName(), e.getMessage());
}
return resp;
}
}测试http



剩下要做的事情就是在前台的html中发起ajax请求访问这个地址,来获取数据并且加载它

在下面就可以进行ajax的访问了,在页面初始化以后,立即发起ajax来进行加载
const App获取当前的应用对象

更新页面

2-5 开发BookService实现图书分页查询
作为图书列表有哪些制约的因素会影响查询结果呢?
选择了不同的类别自然也要在原始数据中进行筛选

勾选了不同的排序方式也会对查询的结果产生影响

![]()
点击加载更多实际上就是一个分页的过程,会把额外的数据在下方额外的加载
图书查询就是对book表中的数据进行筛选
book表包含了9个字段

//完成与数据表的绑定
@TableName("book")
public class Book {
@TableId(type= IdType.AUTO)
private Long bookId;
private String bookName;
private String subTitle;
private String author;
private String cover;
private String description;
private Long categoryId;
private Float evaluationScore;
private Integer evaluationQuantity;
public Long getBookId() {
return bookId;
}
public void setBookId(Long bookId) {
this.bookId = bookId;
}
public String getBookName() {
return bookName;
}
public void setBookName(String bookName) {
this.bookName = bookName;
}
public String getSubTitle() {
return subTitle;
}
public void setSubTitle(String subTitle) {
this.subTitle = subTitle;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getCover() {
return cover;
}
public void setCover(String cover) {
this.cover = cover;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Long getCategoryId() {
return categoryId;
}
public void setCategoryId(Long categoryId) {
this.categoryId = categoryId;
}
public Float getEvaluationScore() {
return evaluationScore;
}
public void setEvaluationScore(Float evaluationScore) {
this.evaluationScore = evaluationScore;
}
public Integer getEvaluationQuantity() {
return evaluationQuantity;
}
public void setEvaluationQuantity(Integer evaluationQuantity) {
this.evaluationQuantity = evaluationQuantity;
}
@Override
public String toString() {
return "Book{" +
"bookId=" + bookId +
", bookName='" + bookName + '\'' +
", subTitle='" + subTitle + '\'' +
", author='" + author + '\'' +
", cover='" + cover + '\'' +
", description='" + description + '\'' +
", categoryId=" + categoryId +
", evaluationScore=" + evaluationScore +
", evaluationQuantity=" + evaluationQuantity +
'}';
}
}
要开发对应的实体类进行映射
创建book实体类对应数据库里的book表


额外新增接口
public interface BookMapper extends BaseMapper<Book> {
}
创建book.xml
<?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.imooc.reader.mapper.BookMapper"> </mapper>
创建bookService接口,说明图书的业务逻辑

public interface BookService {
/**
* 分页查询图书
* @param categoryId 分类编号
* @param order 排序方式
* @param page 页号
* @param rows 每页记录数
* @return 分页对象
*/
public IPage<Book> selectPage(Long categoryId, String order, Integer page, Integer rows);
}
创建对应的实现类

//说明是service
@Service
//开启声明式事务
@Transactional(propagation = Propagation.NOT_SUPPORTED,readOnly = true)
public class BookServiceImpl implements BookService {
@Resource
private BookMapper bookMapper;
@Override
public IPage<Book> selectPage(Long categoryId, String order, Integer page, Integer rows) {
IPage<Book> p = new Page<>(page, rows);
QueryWrapper<Book> wrapper = new QueryWrapper<>();
//从前台传来了有效的分页信息
if(categoryId != null && categoryId != -1){
//如果前台用户确定选择了某一个分类的话,根据分类编号对数据进行筛选
wrapper.eq("category_id", categoryId);
}
if(order != null){
if(order.equals("quantity")){
//根据前台值传来的不同对字段进行降序排列
wrapper.orderByDesc("evaluation_quantity");
}else if(order.equals("score")){
//按分数排序
wrapper.orderByDesc("evaluation_score");
}
}else{
wrapper.orderByDesc("evaluation_quantity");
}
p = bookMapper.selectPage(p, wrapper);
return p;
}
}进行测试生成测试用例类,模拟不同的场景
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class BookServiceTest {
@Resource
private BookService bookService;
/**
* 查询1号分类前10条数据,按热度排序
*/
@Test
public void selectPage1() {
IPage<Book> page = bookService.selectPage(1l, "quantity", 1, 10);
System.out.println("总页数:" + page.getPages());
System.out.println("记录总数:" + page.getTotal());
for(Book book : page.getRecords()){
System.out.println(book.getCategoryId() + "-" + book.getBookName()
+ "-" + book.getEvaluationQuantity() + "-" + book.getEvaluationScore());
}
}
/**
* 查询所有图书前10条数据,按分数排序
*/
@Test
public void selectPage2() {
IPage<Book> page = bookService.selectPage(null, "score", 1, 10);
System.out.println("总页数:" + page.getPages());
System.out.println("记录总数:" + page.getTotal());
for(Book book : page.getRecords()){
System.out.println(book.getCategoryId() + "-" + book.getBookName()
+ "-" + book.getEvaluationQuantity() + "-" + book.getEvaluationScore());
}
}
/**
* 查询所有图书第3页数据,按热度排序
*/
@Test
public void selectPage3() {
IPage<Book> page = bookService.selectPage(null, "quantity", 3, 10);
System.out.println("总页数:" + page.getPages());
System.out.println("记录总数:" + page.getTotal());
for(Book book : page.getRecords()){
System.out.println(book.getCategoryId() + "-" + book.getBookName()
+ "-" + book.getEvaluationQuantity() + "-" + book.getEvaluationScore());
}
}
}2-6 实现图书列表功能
实现控制器controller以及html界面的部分

@RestController
@RequestMapping("/api/book")
public class BookController {
@Resource
private BookService bookService;
@GetMapping("/list")
public ResponseUtils list(Long categoryId , String order ,Integer page){
ResponseUtils resp = null;
try {
IPage<Book> p = bookService.selectPage(categoryId, order, page, 10);
resp = new ResponseUtils().put("page", p);
}catch (Exception e){
e.printStackTrace();
resp = new ResponseUtils(e.getClass().getSimpleName(), e.getMessage());
}
return resp;
}
}
测试

执行成功

接下来结合界面进行开发回到index.html上
页面初始的这些状态数据其实就是用来构建我们刚才url的重要组成部分

前两项因为进行了双向的数据绑定,如果更换了选择项两个数据也会发生相应的变更

页号也是会随着程序的进行来不断的变化,当发生了页码改变以后调用this.onchange来进行数据的重新加载

这个onchange方法就是本节课要开发的重点所在

这里的页面变更主要包含三项,第一个是改变了分类的信息,第二的是改变了排序规则,第三个是改变了页码,这三项有任何一个变化都会触发onchange


针对onchange我们如何进行开发呢?
获取到app对象

定义一个常量uri,对应刚才查询的api接口,接着组织参数第一个参数page代表页号
![]()
查询那一页数据
![]()
当前选择了哪一个分类
![]()
排序

此时就按照controller的要求提供了参数
执行这个ajax
axios.get(uri).then(function(response){
const json = response.data;
if(json.code == "0"){
const list = json.data.page.records;
//这样就把原有的所有数据清除掉了
if(isFlush == true){
objApp.bookList.splice(0, objApp.bookList.length);
}
//进行遍历,调用push方法对数据进行追加,这样最新的数据就会填充到booklist数组中
list.forEach(function(item){
objApp.bookList.push(item);
})
// console.info(list);
//对分页信息进行重新的设置,查询成功后重新赋值 current代表当前页号的意思
objApp.state.page = json.data.page.current;
//判断当前页是否是最后一页 isLastPage决定了分页后是继续显示加载更多的按钮还是显示没有其他数据了的提示
if(json.data.page.pages == json.data.page.current){
objApp.state.isLastPage = true;
}else{
objApp.state.isLastPage = false;
}
}else{
console.error(json);
}
})希望在页面加载完成后就先执行一次
, mounted() {
//页面初始化时执行的操作
const objApp = this;
//true是清空原有数据 就是booklist中的所有数据
this.onchange(true);
axios.get("/api/category/list")
.then(function(response){
const json = response.data;
if(json.code == "0"){
json.data.list.forEach(function(item){
const option = {};
option.text = item.categoryName;
option.value = item.categoryId;
objApp.categoryOptions.push(option);
})
}else{
console.error(json);
}
})
}写完后重启tomcat刷新

原有的数据就消失不见了,因为执行了splice


查询所有数据并且按照热度进行排序

输出了当前页的数据包含了10本书的信息
展开后发现,这本书完整的数据都有体现

我们要做的就是把这10本书的对象依次存储到原有的booklist里就可以了
axios.get(uri).then(function(response){
const json = response.data;
if(json.code == "0"){
const list = json.data.page.records;
//这样就把原有的所有数据清除掉了
if(isFlush == true){
objApp.bookList.splice(0, objApp.bookList.length);
}
//进行遍历,调用push方法对数据进行追加,这样最新的数据就会填充到booklist数组中
list.forEach(function(item){
objApp.bookList.push(item);
})
// console.info(list);
//对分页信息进行重新的设置,查询成功后重新赋值 current代表当前页号的意思
objApp.state.page = json.data.page.current;
//判断当前页是否是最后一页 isLastPage决定了分页后是继续显示加载更多的按钮还是显示没有其他数据了的提示
if(json.data.page.pages == json.data.page.current){
objApp.state.isLastPage = true;
}else{
objApp.state.isLastPage = false;
}
}else{
console.error(json);
}
})测试
![]()
还有一层data没有写

重新加载

点击出现新的数据

直到没有数据

改变查询的规则,进行类型的筛选

就会重新进行数据查询

进行类别的筛选也好还是进行分页的处理本质都是对原有的参数进行的调整
就是需要实现onchange
onchange就是去收集当前页面的各个属性让后将其拼接成一个uri,之后将这些uri传递到控制器,控制器进行数据查询,然后在这里进行展示
,就可以了

2-7 加载图书详情数据

详情页就是在默认的首页上点击任意一本图书,来显示图书具体内容的页面

第一个任务把图书的基本信息进行加载和显示
在bookservice新增一个接口方法用来获取图书的详细信息
/**
* 根据图书编号查询图书对象
* @param bookId 图书编号
* @return 图书对象
*/
public Book selectById(Long bookId);bookServiceimpl里增加对应的实现
@Override
public Book selectById(Long bookId) {
return bookMapper.selectById(bookId);
}在bookcontroller额外增加一个方法
//使用路径变量来进行查询 附加在uri后面的一个变体的数据
@GetMapping("/id/{id}")
//保证参数名与上面一致
public ResponseUtils selectById(@PathVariable("id") Long id){
ResponseUtils resp = null;
try {
Book book = bookService.selectById(id);
//来进行成功时的赋值操作 把图书对象赋值在响应中
resp = new ResponseUtils().put("book",book);
}catch (Exception e){
e.printStackTrace();
resp = new ResponseUtils(e.getClass().getSimpleName(), e.getMessage());
}
return resp;
}进入测试用的http客户端

返回值是200 通过接口得到了具体的数据

controller写好以后向上推进到界面上
将已经提供好的detail界面复制到webapp
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>慕课书评网</title>
<meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0,user-scalable=no">
<!-- 引入样式文件 -->
<link
rel="stylesheet"
href="/assets/vant/index.css"
/>
<!-- 引入 Vue 和 Vant 的 JS 文件 -->
<script src="/assets/vue/vue.global.js"></script>
<script src="/assets/vant/vant.min.js"></script>
<script src="/assets/axios/axios.js"></script>
<style>
.description {
padding: 5px;
}
.description p {
text-indent: 2em;
line-height: 30px;
}
.description img {
width: 100%;
}
</style>
</head>
<body>
<div id="app">
<!--导航栏 -->
<van-nav-bar @click-right="clickRight">
<template #left>
<a href="/" style="padding-top: 10px">
<img src="/images/logo2.png" style="width: 80px">
</a>
</template>
<template #right>
<template v-if = "state.isLogin">
<img src="/images/user_icon.png" style="height:30px">{
{member.nickname}}
</template>
<template v-if = "!state.isLogin">
<img src="/images/user_icon.png" style="height: 30px">
登录
</template>
</template>
</van-nav-bar>
<van-row style="padding: 10px;color:white;font-size: 80%;background: rgb(127, 125, 121)">
<van-col span="8" style="float: left;width: 110px;height: 160px">
<img style="width: 110px;height: 160px"
:src="book.cover">
</van-col>
<van-col span="16" style="float: left;height: 160px;width:auto">
<div style="font-size: 16px;font-weight: bold">{
{book.bookName}}</div>
<div style="margin-top:5px;background: #92B8B1;padding: 5px">{
{book.author}}</div>
<div style="font-size: 16px;margin-top:5px;">{
{book.subTitle}}</div>
<div style="padding:10px">
<template v-if="state.readState == -1">
<van-button size="small" icon="like-o" type="default" style="margin-right: 10px" @click="updateReadState(1)">想看</van-button>
<van-button size="small" icon="passed" type="default" @click="updateReadState(2)">看过</van-button>
</template>
<template v-if="state.readState == 1">
<van-button size="small" icon="like" type="success" style="margin-right: 10px" @click="updateReadState(1)">想看</van-button>
<van-button size="small" icon="passed" type="default" @click="updateReadState(2)">看过</van-button>
</template>
<template v-if="state.readState == 2">
<van-button size="small" icon="like-o" type="default" style="margin-right: 10px" @click="updateReadState(1)">想看</van-button>
<van-button size="small" icon="checked" type="success" @click="updateReadState(2)">看过</van-button>
</template>
</div>
</van-col>
<van-col span="24" style="background-color: rgba(0,0,0,0.1);padding: 10px;margin-top: 10px">
<span style="line-height: 20px;margin-right: 10px">{
{book.evaluationScore}}分 {
{book.evaluationQuantity}}人已评</span>
<van-rate v-model="book.evaluationScore" color="#ffd21e" void-icon="star" readonly allow-half></van-rate>
</van-col>
</van-row>
<div class="description" v-html="book.description"></div>
<van-nav-bar style="background: lightblue">
<template #left>
短评
</template>
<template #right>
<van-button type="success" size="small" style="width:60px" @click="showDialog()">评价</van-button>
</template>
</van-nav-bar>
<template v-for="evaluation,index in evaluationList">
<div style="border-bottom: 1px solid #cccccc">
<div style="padding: 10px;">
<span style="margin-right: 20px">{
{evaluation.strCreateTime}}</span>
<span style="margin-right: 20px">{
{evaluation.member.nickname}}</span>
<van-rate v-model="evaluation.score" color="#ffd21e" void-icon="star" readonly allow-half></van-rate>
<van-button icon="like-o" @click="enjoy(index)" type="success" size="small" style="width:40px;float:right" >{
{evaluation.enjoy}}</van-button>
</div>
<div style="padding: 10px;">
{
{evaluation.content}}
</div>
</div>
</template>
<div style="margin-bottom: 50px">
</div>
<van-action-sheet v-model:show="state.dialogVisible" :title="book.bookName">
<van-form ref="evaluationForm">
<van-cell-group inset>
<van-rate v-model="form.score" color="#ffd21e" void-icon="star" style="padding: 16px" ></van-rate>
<van-field
v-model="form.content"
name="content"
placeholder="这里输入评论内容"
autocomplete = "off"
:rules="[{ required: true, message: '请输入评论内容' }]"
/>
</van-cell-group>
<div style="margin: 16px;margin-bottom: 50px">
<van-button round block type="primary" @click="onSubmit('evaluationForm')">
提交短评
</van-button>
</div>
</van-form>
</van-action-sheet>
</div>
<script>
//日期格式化函数
function formatDate(time){
var newDate = new Date(time);
return (newDate.getMonth() + 1) + "-" + newDate.getDate();
}
//获取查询字符串函数
function getQueryString(name) {
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
var r = window.location.search.substr(1).match(reg);
if (r != null) {
return unescape(r[2]);
}
return null;
}
//提示"需要登录"对话框
function warnLogin(text){
vant.Dialog.confirm({
title: text,
confirmButtonText: "去登录",
cancelButtonText: "关闭"
})
.then(() => {
window.location.href = "/login.html";
})
}
const main = {
data() {
return {
state : { //页面状态
isLogin: false, //是否登录
member:{}, //当前登录会员数据
readState : -1, //当前会员阅读状态 -1:无数据 1:想看 2:看过
dialogVisible : false //短评对话框是否显示
},
book: {
},
evaluationList:[], //短评列表
form: { //短评表单数据
score:5,
content:""
}
}
}
, methods: {
//显示短评对话框
showDialog:function(){
const objApp = this;
if(!objApp.state.isLogin){
warnLogin("登录后才能进行评论哦");
return;
}
this.state.dialogVisible = true;
}
,clickRight:function(){
//点击"登录"按钮跳转到登录页
if(!this.state.isLogin){
window.location.href = "/login.html";
}
}
,updateReadState:function(readState){
//更新阅读状态
}
,onSubmit: function (formName) {
//提交短评功能
}
,enjoy : function(index){
//点赞功能
}
}
, mounted() {
//初始化后数据操作
const bookId = getQueryString("bid");
const objApp = this;
const uri = "/api/book/id/" + bookId;
axios.get(uri).then(function(response){
const json = response.data;
if(json.code == "0"){
objApp.book = json.data.book;
}else{
console.error(json);
}
})
}
};
const app = Vue.createApp(main);
app.use(vant);
app.use(vant.Lazyload);
app.mount("#app");
</script>
</body>
</html>2-8 加载图书短评数据
本节来加载下方的短评列表

短评表是没有用户昵称的 昵称nickname是在用户表里才会出现的,如何在查询短评的时候一次性的把用户昵称也带过来呢?这就涉及到关联查询了回顾一下通过mybatis来实现关联查询的
<!--短评列表,通过迭代EvaluationList实现-->
<template v-for="evaluation,index in evaluationList">
<div style="border-bottom: 1px solid #cccccc">
<div style="padding: 10px;">
<!-- 短评创建时间 -->
<span style="margin-right: 20px">{
{evaluation.ct}}</span>
<!-- 用户昵称 -->
<span style="margin-right: 20px">{
{evaluation.nickname}}</span>
<!-- 星型分数 -->
<van-rate v-model="evaluation.score" color="#ffd21e" void-icon="star" readonly allow-half></van-rate>
<van-button icon="like-o" @click="enjoy(index)" type="success" size="small" style="width:40px;float:right" >{
{evaluation.enjoy}}</van-button>
</div>
<!-- 评论内容 -->
<div style="padding: 10px;">
{
{evaluation.content}}
</div>
</div>
</template>创建实体类evaluation实体类

@TableName("evaluation")
public class Evaluation {
@TableId(type = IdType.AUTO)
private Long evaluationId;
private Long bookId;
private String content;
private Integer score;
private Long memberId;
private Date createTime;
private Integer enjoy;
private String state;
private String disableReason;
private Date disableTime;
public Long getEvaluationId() {
return evaluationId;
}
public void setEvaluationId(Long evaluationId) {
this.evaluationId = evaluationId;
}
public Long getBookId() {
return bookId;
}
public void setBookId(Long bookId) {
this.bookId = bookId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Integer getScore() {
return score;
}
public void setScore(Integer score) {
this.score = score;
}
public Long getMemberId() {
return memberId;
}
public void setMemberId(Long memberId) {
this.memberId = memberId;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Integer getEnjoy() {
return enjoy;
}
public void setEnjoy(Integer enjoy) {
this.enjoy = enjoy;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getDisableReason() {
return disableReason;
}
public void setDisableReason(String disableReason) {
this.disableReason = disableReason;
}
public Date getDisableTime() {
return disableTime;
}
public void setDisableTime(Date disableTime) {
this.disableTime = disableTime;
}
}新增接口

为什么要在这里增加一个方法按图书编号查询短评数据同时返回的类型还是一个map呢?
public interface EvaluationMapper extends BaseMapper<Evaluation> {
public List<Map> selectByBookId(Long bookId);
}
作为我们查询短评列表的过程中它要包含用户的昵称而用户的昵称实际上在原有的短评表里是不存在的为了获取到额外的信息我们需要关联查询,关联查询我们可以使用map来将每一个结果进行包装。这样我们的sql既具备了可扩展性同时map结构使用起来也比较灵活,作为日常开发中遇到多表关联查询通过map承载记录是一个常见的做法
创建xml
<?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.imooc.reader.mapper.EvaluationMapper">
<select id="selectByBookId" parameterType="Long" resultType="java.util.LinkedHashMap">
select e.*,m.nickname from evaluation e , member m where e.member_id = m.member_id and e.book_id = #{value} order by e.create_time desc
</select>
</mapper>生成测试用例
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class EvaluationMapperTest {
@Resource
private EvaluationMapper evaluationMapper;
@Test
public void selectByBookId() {
List<Map> maps = evaluationMapper.selectByBookId(1l);
System.out.println(maps);
}
}运行
![]()
新增evaluationService接口
public interface EvaluationService {
public List<Map> selectByBookId(Long bookId);
}
创建对应的实现类
@Service
@Transactional(propagation = Propagation.NOT_SUPPORTED,readOnly = true)
public class EvaluationServiceImpl implements EvaluationService {
@Resource
private EvaluationMapper evaluationMapper;
@Override
public List<Map> selectByBookId(Long bookId) {
return evaluationMapper.selectByBookId(bookId);
}
}
新建控制器evaluationController
@RestController
@RequestMapping("/api/evaluation")
public class EvaluationController {
@Resource
private EvaluationService evaluationService;
@GetMapping("/list")
public ResponseUtils list(Long bookId){
ResponseUtils resp = null;
try {
List<Map> maps = evaluationService.selectByBookId(bookId);
resp = new ResponseUtils().put("list",maps);
}catch (Exception e){
e.printStackTrace();
resp = new ResponseUtils(e.getClass().getSimpleName(), e.getMessage());
}
return resp;
}
}生成http测试类
![]()

可以看到所有关于1号图书的短评都被提取了出来

包含在list里存放在data这个选项中
回到detail.html的界面上对其进行加载和显示
发起全新的get请求获取短评列表
axios.get("/api/evaluation/list?bookId=" + bookId)
.then(function(response){
const json = response.data;
if(json.code == "0"){
const list = json.data.list;
list.forEach(function(item){
item.ct = formatDate(item.create_time);
objApp.evaluationList.push(item);
})
}else{
console.error(json);
}
})
}执行完以后就相当于evaluationlist这个数据集就有了数据,就可以在上方进行动态渲染了
运行

发起了两个ajax请求
第一个请求获取图书的基本信息,第二个查询了短评的数据

下方可以看到短评的数据被成功加载显示了出来
还有个问题是缺少发布时间
发布时间是ct这个属性
但是在原始返回的数据中并没有ct这一项,ct是需要我们手动加工的,在不同应用中关于时间的表达形式是不一样的,直接提取原始的createtime创建时间,在界面上显示的格式肯定不对,为了解决这个问题,我们在上方封装一个显示时间的方法
传入具体的时间后会返回特定的时间字符串,我们现在的需要只用返回月和日就可以了
附加进行日期的格式化

在最下方 关于时间就显示出来了

2-9 【选看】解决BUG:LocalDateTime异常的出现与解决方法
编写完接口后,在输出类型中包含了时间类型得到的结果是不完整的

![]()
这和mybatis的版本是有关系的在3.4.5这个版本的前后有一个重大的变更就是对日期类型是否采用localDateTime。当前异常只有你引入了较新版本的mybatis才会产生
如何看到它的版本?



根本原因是根据版本的不同底层作为日期存储的对象发生了变化,作为前台的序列化组件对新版本产生的localDateTime支持的不好所以就出现错误了,怎么解决?
自己增加一个转换器类告诉json如何处理localdateTime就可以了
新增config配置包

新增一个类
转换器
增加一个自定义的json对象转换器
打开applicationContext.xml进行配置

重启

2-10 【同2-9】解决Bug:LocalDateTime异常的出现与解决方法

<!--MyBatisPlus插件-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.4.3.1</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
<scope>compile</scope>
</dependency>


import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
public class CustomLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
public void serialize(LocalDateTime dateTime, JsonGenerator generator, SerializerProvider sp)
throws IOException, JsonProcessingException {
long time = dateTime.toInstant(ZoneOffset.of("+8")).toEpochMilli();
generator.writeNumber(time);
}
}

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.PackageVersion;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Locale;
import java.util.TimeZone;
public class CustomObjectMapper extends ObjectMapper {
public CustomObjectMapper() {
this.registerModule(new JavaTimeModule());
}
public class JavaTimeModule extends SimpleModule {
public JavaTimeModule() {
super(PackageVersion.VERSION);
this.addSerializer(LocalDateTime.class, new CustomLocalDateTimeSerializer());
}
}
}
![]()
<!--开启MVC注解开发,响应字符集-->
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper">
<bean class="com.imooc.reader.config.CustomObjectMapper"/>
</property>
</bean>
....
</mvc:message-converters>
</mvc:annotation-driven>
边栏推荐
- Leetcode54. Spiral matrix
- QingChuang technology joined dragon lizard community to build a new ecosystem of intelligent operation and maintenance platform
- SSTI template injection
- [MRCTF2020]Ezpop 1
- C event usage case subscription event+=
- Dasctf2022.07 enabling game password WP
- Risk control and application of informatization project
- Graph node deployment and testing
- XxE & XML vulnerability
- 数据提取2
猜你喜欢
随机推荐
一文速览EMNLP 2020中的Transformer量化论文
All in one 1353 -- expression bracket matching (stack)
I can't figure out why MySQL uses b+ trees for indexing?
How does kettle handle text data transfer as' 'instead of null
Design and development of GUI programming for fixed-point one click query
The third letter to the little sister of the test | Oracle stored procedure knowledge sharing and test instructions
一段平平无奇的秋招经历
C event usage case subscription event+=
redis配置文件下载
pytorch_ demo1
帮忙发几个招聘,有兴趣可以看看
QT creator code style plug-in beautifier
End of year summary
"PHP Basics" tags in PHP
The code interface is a little automated
数据提取2
Use of "PHP Basics" Boolean
Leetcode54. Spiral matrix
"PHP Basics" use of integer data
二零二零年终总结









