续 显示问题列表
上次课中显示问题列表中的用户昵称位置属性编写错了
需要修改为
<small class="list-inline-item"
v-text="question.userNickName">风继续吹</small>
显示问题持续时间
现在流行的处理问题时间的方式不是单纯的显示这个问题的提问时间
而是显示出这个问题出现了多久可能又一下情况
- 刚刚(1分钟之内)
- XX分钟前(60分钟以内)
- XX小时前(24小时以内)
- XX天前
由于时间是数据库中保存好的信息,这个信息已经以JSON格式发送到了ajax中
所以添加这个功能不需要编写后台代码
首先在index.js文件中添加一个计算持续时间的方法
updateDuration,并在ajax中调用
代码如下
/*
显示当前用户的问题
*/
let questionsApp = new Vue({
el:'#questionsApp',
data: {
questions:[]
},
methods: {
loadQuestions:function () {
$.ajax({
url: '/v1/questions/my',
method: "GET",
success: function (r) {
console.log("成功加载数据");
console.log(r);
if(r.code === OK){
questionsApp.questions = r.data;
//调用计算持续时间的方法
questionsApp.updateDuration();
}
}
});
},
updateDuration:function () {
let questions=this.questions;
for(let i=0;i<questions.length;i++){
//获得问题中的创建时间属性(毫秒数)
let createtime=new Date(questions[i].createtime).getTime();
//获得当前时间的毫秒数
let now=new Date().getTime();
//计算时间差(秒)
let durtaion=(now-createtime)/1000;
if(durtaion<60){
// 显示刚刚
//duration这个名字可以随便起,只要保证和页面上取的一样就行
questions[i].duration="刚刚";
}else if(durtaion<60*60){
// 显示XX分钟
questions[i].duration=
(durtaion/60).toFixed(0)+"分钟前";
}else if (durtaion<60*60*24){
//显示XX小时
questions[i].duration=
(durtaion/60/60).toFixed(0)+"小时前";
}else{
//显示XX天
questions[i].duration=
(durtaion/60/60/24).toFixed(0)+"天前";
}
}
}
},
created:function () {
console.log("执行了方法");
this.loadQuestions(1);
}
});
Index.html页面也需要进行一个修改,让计算出的持续时间显示出来
代码如下
<small class="list-inline-item"
v-text="question.duration">13分钟前</small>
显示问题的标签列表
页面中的问题是可以多个标签的
怎么实现显示多个标签呢?
首先来了解一下标签和问题的对应关系
我们可以看到,在问题表中我们保持了冗余的数据列tag_names,这么做的好处就是减少查询时的复杂度,实际开发中程序员们也可能用这样的方式
实现过程
实现思路
1.创建一个包含全部标签的Map,map的key是标签名称,value是标签对象
2.从问题实体类中获得tag_names属性,利用字符串的split方法,拆分成字符串数组
3.遍历字符串数组,从Map中通过key(标签名称)获得value(标签对象)
4.将获取的value存入Question实体类中的List<Tag>tags属性
步骤1:
我们在Question实体类中需要定义一个List<Tag> tags
原因是我们需要能够从一个问题中获得多个标签
//为问题实体类添加标签集合
//@TableField(exist = false)表示数据库中没有这样的列,防止报错
@TableField(exist = false)
private List tags;
步骤2:
业务逻辑层添加方法得到包含所有Tag标签的Map
ITagService
public interface ITagService extends IService<Tag> {
//获得所有标签的方法
List<Tag> getTags();
//获得所有标签返回Map的方法
Map<String,Tag> getName2TagMap();
}
步骤3:
实现这个方法
package cn.tedu.straw.portal.service.impl;
import cn.tedu.straw.portal.model.Tag;
import cn.tedu.straw.portal.mapper.TagMapper;
import cn.tedu.straw.portal.service.ITagService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* <p>
* 服务实现类
* </p>
*
* @author tedu.cn
* @since 2020-12-09
*/
@Service
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements ITagService {
//CopyOnWriteArrayList<>是线程安全的集合,适合在高并发的环境下使用
private final List<Tag> tags=new CopyOnWriteArrayList<>();
//ConcurrentHashMap是线程安全的Map,适合在高并发的环境下使用
private final Map<String,Tag> map=new ConcurrentHashMap<>();
@Override
public List<Tag> getTags() {
//这个if主要是为了保证tags被顺利赋值之后的高效运行
if(tags.isEmpty()) {
synchronized (tags) {
//这个if主要是为了保证不会有两条以上线程为tags重复添加内容
if (tags.isEmpty()) {
//super.list()是父类提供的查询当前指定实体类全部行的代码
tags.addAll(super.list());
//为所有标签赋值List类型之后,可以同步给map赋值
for(Tag t: tags){
//将tags中所有标签赋值给map
//而map的key是tag的name,value就是tag
map.put(t.getName(),t);
}
}
}
}
return tags;
}
@Override
public Map<String, Tag> getName2TagMap() {
//判断如果map是空,证明上面getTags方法没有运行
if(map.isEmpty()){
//那么就调用上面的getTags方法
getTags();
}
return map;
}
}
步骤4:
在QuestionServiceImpl类中编写代码
将数据库tag_names列中的内容转换成List<Tag>
//根据Question的tag_names列的值,返回List<Tag>
private List<Tag> tagNamesToTags(String tagNames){
//得到的tag_name拆分字符串
//tagNames="java基础,javaSE,面试题"
String[] names=tagNames.split(",");
//names={"java基础","javaSE","面试题"}
//声明List以便返回
List<Tag> list=new ArrayList<>();
Map<String,Tag> map=tagService.getName2TagMap();
//遍历String数组
for(String name:names) {
//根据String数组中当前的元素获得Map对应的value
Tag tag=map.get(name);
//将这个value保存在list对象中
list.add(tag);
}
return list;
}
步骤5:
在我们编写的QuestionServiceImpl类中的getMyQuestions方法中
根据步骤4中编写的方法来获得Question对象中的List<Tag>并赋值
编写完毕后QuestionServiceImpl完整代码如下!
@Service
@Slf4j
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {
@Autowired
IUserService userService;
@Autowired
UserMapper userMapper;
@Autowired
QuestionMapper questionMapper;
//按登录用户查询当前用户问题的方法
@Override
public List<Question> getMyQuestions() {
//获得当前登录用户的用户名
String username=userService.currentUsername();
log.debug("当前登录用户为:{}",username);
//如果已经登录,使用之前编写好的findUserByUsername方法
//查询出当前用户的详细信息(实际上主要需要用户的id)
User user=userMapper.findUserByUsername(username);
if(user == null){
throw ServiceException.gone("登录用户不存在");
}
log.debug("开始查询{}用户的问题",user.getId());
QueryWrapper<Question> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("user_id",user.getId());
queryWrapper.eq("delete_status",0);
queryWrapper.orderByDesc("createtime");
List<Question> list=questionMapper.selectList(queryWrapper);
log.debug("当前用户的问题数量为:{}",list.size());
//遍历当前查询出的所有问题对象
for(Question q: list){
//将问题每个对象的对应的Tag都查询出来,并赋值为实体类中的List<Tag>
List<Tag> tags=tagNamesToTags(q.getTagNames());
q.setTags(tags);
}
return list;
}
@Autowired
ITagService tagService;
//根据Question的tag_names列的值,返回List<Tag>
private List<Tag> tagNamesToTags(String tagNames){
//得到的tag_name拆分字符串
//tagNames="java基础,javaSE,面试题"
String[] names=tagNames.split(",");
//names={"java基础","javaSE","面试题"}
//声明List以便返回
List<Tag> list=new ArrayList<>();
Map<String,Tag> map=tagService.getName2TagMap();
//遍历String数组
for(String name:names) {
//根据String数组中当前的元素获得Map对应的value
Tag tag=map.get(name);
//将这个value保存在list对象中
list.add(tag);
}
return list;
}
}
步骤6:
最后修改一下html页面内容,来获取问题的标签
<a class="text-info badge badge-pill bg-light"
href="tag/tag_question.html" v-for="tag in question.tags">
<small v-text="tag.name" >Java基础 </small>
</a>
显示问题的图片
现在项目中每个问题右侧跟一个图片
这个图片实际上是根据问题的第一个标签的id来决定的
需要在index.js文件中编写显示相关图片的代码
并在合适位置调用
代码如下
/*
显示当前用户的问题
*/
let questionsApp = new Vue({
el:'#questionsApp',
data: {
questions:[]
},
methods: {
loadQuestions:function () {
$.ajax({
url: '/v1/questions/my',
method: "GET",
success: function (r) {
console.log("成功加载数据");
console.log(r);
if(r.code === OK){
questionsApp.questions = r.data;
//调用计算持续时间的方法
questionsApp.updateDuration();
//调用显示所有按标签呈现的图片
questionsApp.updateTagImage();
}
}
});
},
updateTagImage:function(){
let questions = this.questions;
for(let i=0; i<questions.length; i++){
//获得当前问题对象的所有标签的集合(数组)
let tags = questions[i].tags;
//js代码中特有的写法if(tags)
//相当于判断tags非空
if(tags){
//获取当前问题的第一个标签对应的图片文件路径
let tagImage = 'img/tags/'+tags[0].id+'.jpg';
console.log(tagImage);
//将这个文件路径保存到tagImage属性用,以便页面调用
questions[i].tagImage = tagImage;
}
}
},
updateDuration:function () {
let questions=this.questions;
for(let i=0;i<questions.length;i++){
//获得问题中的创建时间属性(毫秒数)
let createtime=new Date(questions[i].createtime).getTime();
//获得当前时间的毫秒数
let now=new Date().getTime();
//计算时间差(秒)
let durtaion=(now-createtime)/1000;
if(durtaion<60){
// 显示刚刚
//duration这个名字可以随便起,只要保证和页面上取的一样就行
questions[i].duration="刚刚";
}else if(durtaion<60*60){
// 显示XX分钟
questions[i].duration=
(durtaion/60).toFixed(0)+"分钟前";
}else if (durtaion<60*60*24){
//显示XX小时
questions[i].duration=
(durtaion/60/60).toFixed(0)+"小时前";
}else{
//显示XX天
questions[i].duration=
(durtaion/60/60/24).toFixed(0)+"天前";
}
}
}
},
created:function () {
console.log("执行了方法");
this.loadQuestions(1);
}
});
在index.html文件中绑定
<img src="img/tags/example0.jpg"
v-bind:src="question.tagImage"
class="ml-3 border img-fluid rounded"
alt="" width="208" height="116">
测试即可
实现分页功能
为什么需要分页(翻页)
- 不会一次显示太多内容,不会产生大量流量,对服务器压力小
- 我们需要的信息,往往在前面几条的内容,后面的内容使用率不高
- 用户体验强,方便记忆位置
实现分页的sql语句
主要通过limit关键字实现分页查询
只查询userid为11的学生提问的前8条内容
select id,title from question
where user_id=11 order by createtime desc limit 0,8
使用上面的sql语句可以实现分页功能
但是所有信息都需要自己计算,而且计算的方式是固定的,
所以Mybatis提供了一套自动完成计算的翻页组件
PageHelper
PageHelper的使用
步骤1:
由于PageHelper是Mybatis提供的,没有SpringBoot的默认版本支持
所以像Mybatis一眼我们要自己管理版本
在Straw父项目的pom.xml文件中添加如下代码
<properties>
<java.version>1.8</java.version>
<mybatis.plus.version>3.3.1</mybatis.plus.version>
<pagehelper.starter.version>1.3.0</pagehelper.starter.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper.starter.version}</version>
</dependency>
<!-- 其它略 -->
</dependencies>
</dependencyManagement>
步骤2:
子项目pom.xml文件添加代码
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
注意父子项目的pom.xml都需要刷新!
步骤3:
原理上
PageHelper.startPage([页码],[每页条数])
PageHelper.startPage(1, 8);
代码中一旦出现了上面的设置
之后的第一个查询,就会按照我们设置的方式进行分页查询
但是查询结果的类型需要变化,变为PageInfo
PageInfo这个类包含了各种分页的信息,和通常情况下我们查询的List
实现分页代码如下
因为返回值类型变了,所以要从业务逻辑层开始重构(修改)
IQuestionService接口重构
public interface IQuestionService extends IService<Question> {
//按登录用户查询当前用户问题的方法
PageInfo<Question> getMyQuestions(
Integer pageNum,Integer pageSize
);
}
步骤4:
QuestionServiceImpl重构接口中的方法
@Override
public PageInfo<Question> getMyQuestions(
//传入翻页查询的参数
Integer pageNum,Integer pageSize
) {
//分页查询,决定查询的页数
if(pageNum==null || pageSize==null){
//分页查询信息不全,直接抛异常
throw ServiceException.invalidRequest("参数不能为空");
}
//获得当前登录用户的用户名
String username=userService.currentUsername();
log.debug("当前登录用户为:{}",username);
//如果已经登录,使用之前编写好的findUserByUsername方法
//查询出当前用户的详细信息(实际上主要需要用户的id)
User user=userMapper.findUserByUsername(username);
if(user == null){
throw ServiceException.gone("登录用户不存在");
}
log.debug("开始查询{}用户的问题",user.getId());
QueryWrapper<Question> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("user_id",user.getId());
queryWrapper.eq("delete_status",0);
queryWrapper.orderByDesc("createtime");
//执行查询之前,要设置分页查询信息
PageHelper.startPage(pageNum,pageSize);
//紧接着的查询就是按照上面分页配置的分页查询
List<Question> list=questionMapper.selectList(queryWrapper);
log.debug("当前用户的问题数量为:{}",list.size());
//遍历当前查询出的所有问题对象
for(Question q: list){
//将问题每个对象的对应的Tag都查询出来,并赋值为实体类中的List<Tag>
List<Tag> tags=tagNamesToTags(q.getTagNames());
q.setTags(tags);
}
return new PageInfo<Question>(list);
}
步骤5:
尝试一下测试
这个测试时包含指定SpringSecurity用户的
新建一个QuestionTest进行测试
@SpringBootTest
public class QuestionTest {
@Autowired
IQuestionService questionService;
@Test
//@WithMockUser是Spring-Security提供的注解
//在测试中如果需要从Spring-Security中获得用户信息,那么就可以用这个注解标记
//指定用户信息,也要注意,这只是个测试,Spring-Security不会对信息验证
@WithMockUser(username = "st2",password = "123456")
public void getQuest(){
PageInfo<Question> pi=
questionService.getMyQuestions(1,8);
for(Question q:pi.getList()){
System.out.println(q);
}
}
}
控制器的调用和VUE代码的重构
首先重构QuestionController
代码如下
@RestController
@RequestMapping("/v1/questions")
@Slf4j
public class QuestionController {
@Autowired
IQuestionService questionService;
//查询返回当前登录用户发布的问题
@GetMapping("/my")
public R<PageInfo<Question>> my(Integer pageNum){
if(pageNum==null){
pageNum=1;
}
int pageSize=8;
log.debug("开始查询当前用户的问题");
//这里要处理个异常,因为用户可能没有登录
try{
PageInfo<Question> questions=
questionService.getMyQuestions(pageNum,pageSize);
return R.ok(questions);
}catch (ServiceException e){
log.error("用户查询问题失败!",e);
return R.failed(e);
}
}
}
然后编写index.js页面代码的重构
let questionsApp = new Vue({
el:'#questionsApp',
data: {
questions:[],
pageInfo:{}
},
methods: {
loadQuestions:function (pageNum) {
if(!pageNum){ //如果pageNum为空,默认页码为1
pageNum=1;
}
$.ajax({
url: '/v1/questions/my',
method: "GET",
data:{pageNum:pageNum},
success: function (r) {
console.log("成功加载数据");
console.log(r);
if(r.code === OK){
questionsApp.questions = r.data.list;
//调用计算持续时间的方法
questionsApp.updateDuration();
//调用显示所有按标签呈现的图片
questionsApp.updateTagImage();
questionsApp.pageInfo=r.data;
}
}
});
},
//之后代码未修改,略
}
分页导航条的配置
实现了查询第一页的内容
但是现在还不能翻页
需要配置页面给定的分页导航条
代码如下
<div class="pagination">
<a class="page-item page-link" href="#"
v-on:click.prevent="loadQuestions(pageInfo.prePage)"
>上一页</a>
<a class="page-item page-link " href="#"
v-for="n in pageInfo.navigatepageNums"
v-on:click.prevent="loadQuestions(n)"
v-bind:class="
{'bg-secondary text-light':n == pageInfo.pageNum}"
><span v-text="n">1</span></a>
<a class="page-item page-link" href="#"
v-on:click.prevent="loadQuestions(pageInfo.nextPage)"
>下一页</a>
</div>
参考资料
PageInfo类中的常用属性
//当前页
private int pageNum;
//每页的数量
private int pageSize;
//当前页的行数量
private int size;
//当前页面第一个元素在数据库中的行号
private int startRow;
//当前页面最后一个元素在数据库中的行号
private int endRow;
//总页数
private int pages;
//前一页页号
private int prePage;
//下一页页号
private int nextPage;
//是否为第一页
private boolean isFirstPage;
//是否为最后一页
private boolean isLastPage;
//是否有前一页
private boolean hasPreviousPage;
//是否有下一页
private boolean hasNextPage;
//导航条中页码个数
private int navigatePages;
//所有导航条中显示的页号
private int[] navigatepageNums;
//导航条上的第一页页号
private int navigateFirstPage;
//导航条上的最后一页号
private int navigateLastPage;