基于Spring Boot+Vue的博客系统 13——评论功能的实现
需求评论分为匿名评论和实名评论,匿名评论的评论者(也就是commentator字段)的值为0,头像为默认头像,用户登录之后可以进行实名评论在文章评论下面,还可以对一级评论进行评论,一级评论和二级评论的区别在于type字段的不同,一级评论为1,二级评论为2后端设计新建com.qianyucc.blog.model.dto.CommentDTO类,用于封装前端向后端传输的评论信息pa...
·
废弃说明:
这个专栏的文章本意是记录笔者第一次搭建博客的过程,文章里里有很多地方的写法都不太恰当,现在已经废弃,关于SpringBoot + Vue 博客系列,笔者重新写了这个系列的文章,不敢说写的好,但是至少思路更加清晰,还在看SpringBoot + Vue 博客系列文章的朋友可以移步:https://blog.csdn.net/li3455277925/category_10341110.html,文章中有错误的地方或者大家有什么意见或建议都可以评论或者私信交流。
需求
- 评论分为匿名评论和实名评论,匿名评论的评论者(也就是
commentator
字段)的值为0,头像为默认头像,用户登录之后可以进行实名评论 - 在文章评论下面,还可以对一级评论进行评论,一级评论和二级评论的区别在于
type
字段的不同,一级评论为1,二级评论为2
后端设计
- 新建
com.qianyucc.blog.model.dto.CommentDTO
类,用于封装前端向后端传输的评论信息
package com.qianyucc.blog.model.dto;
import lombok.*;
/**
* @author lijing
* @date 2019-10-13 10:53
* @description 前端向后端传输的评论信息
*/
@Data
public class CommentDTO {
private Long id;
private Long parentId;
private Long commentator;
private String content;
private Integer type;
}
- 新建
com.qianyucc.blog.model.vo.CommentVO
类,用于封装后端返回到前端的评论信息
package com.qianyucc.blog.model.vo;
import lombok.*;
import java.util.*;
/**
* @author lijing
* @date 2019-10-13 11:18
* @description 封装返回到前端的评论信息
*/
@Data
public class CommentVO {
private Long id;
private Long parentId;
private Integer type;
private Long commentator;
private String content;
private String gmtCreate;
private String gmtUpdate;
private Long likes;
private Long comments;
private String commentatorAvatarUrl;
private String commentatorName;
private List<CommentVO> secondLevelComments;
}
- 新建
CommentService
编写业务层代码,需要注意的是在插入评论的时候要将父级(这里的父级评论数是文章的评论数或者一级评论的评论数)的评论数加一
package com.qianyucc.blog.service;
import cn.hutool.core.bean.*;
import com.qianyucc.blog.model.dto.*;
import com.qianyucc.blog.model.entity.*;
import com.qianyucc.blog.repository.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.stereotype.*;
import java.util.*;
/**
* @author lijing
* @date 2019-10-13 10:55
* @description 与评论相关的业务
*/
@Service
public class CommentService {
@Autowired
private ArticleRepository articleRepository;
@Autowired
private CommentRepository commentRepository;
@Autowired
private UserRepository userRepository;
/**
* 插入评论
*
* @param commentDTO
* @return
*/
public void insComment(CommentDTO commentDTO) {
// 先将父级评论数或者文章评论数加一
if (commentDTO.getType().equals(1)) {
Optional<ArticleDO> byId = articleRepository.findById(commentDTO.getParentId());
byId.ifPresent(articleDO -> {
articleDO.setComments(articleDO.getComments()+1);
articleRepository.save(articleDO);
});
} else if (commentDTO.getType().equals(2)) {
Optional<CommentDO> byId = commentRepository.findById(commentDTO.getParentId());
byId.ifPresent(commentDO -> {
commentDO.setComments(commentDO.getComments() + 1);
commentRepository.save(commentDO);
});
}
CommentDO commentDO = new CommentDO();
BeanUtil.copyProperties(commentDTO, commentDO);
commentDO.setComments(0L);
commentDO.setLikes(0L);
commentDO.setGmtCreate(System.currentTimeMillis());
commentDO.setGmtUpdate(commentDO.getGmtCreate());
commentRepository.save(commentDO);
}
/**
* 根据文章的id查询该文章的所有评论
*
* @param articleId
* @return
*/
public List<CommentVO> findCommentByArticleId(Long articleId) {
List<CommentDO> commentDOS = commentRepository.findByParentIdAndType(articleId, 1);
List<CommentVO> commentVOS = CommentUtil.jpaDosToVos(commentDOS, userRepository);
// 查找所有一级评论对应的二级评论
commentVOS.forEach(commentVO -> {
List<CommentDO> secondLevelCommentDOS = commentRepository.findByParentIdAndType(commentVO.getId(), 2);
List<CommentVO> secondLevelCommentAOS = CommentUtil.jpaDosToVos(secondLevelCommentDOS, userRepository);
commentVO.setSecondLevelComments(secondLevelCommentAOS);
});
return commentVOS;
}
}
上面的业务层代码用到了自定义工具类
CommentUtil
,并且在CommentRepository
里面定义根据parentId
和type
字段查询的方法
package com.qianyucc.blog.repository;
import com.qianyucc.blog.model.entity.*;
import org.springframework.data.jpa.repository.*;
import java.util.*;
/**
* @author lijing
* @date 2019-10-11 10:40
* @description 访问数据库中评论
*/
public interface CommentRepository extends JpaRepository<CommentDO, Long>, JpaSpecificationExecutor<CommentDO> {
List<CommentDO> findByParentIdAndType(Long articleId, Integer type);
}
package com.qianyucc.blog.utils;
import cn.hutool.core.bean.*;
import com.qianyucc.blog.model.entity.*;
import com.qianyucc.blog.model.vo.*;
import com.qianyucc.blog.repository.*;
import java.util.*;
/**
* @author lijing
* @date 2019-10-13 11:19
* @description 与评论相关的工具方法
*/
public class CommentUtil {
private static final String DATE_PATTERN = "yyyy-MM-dd HH:mm";
/**
* 将model转换为ao,并格式化日期
*
* @param commentDOS
* @return
*/
public static List<CommentVO> jpaDosToVos(List<CommentDO> commentDOS, UserRepository userRepository) {
ArrayList<CommentVO> commentVOS = new ArrayList<>();
commentDOS.forEach(commentDO -> {
CommentVO commentVO = new CommentVO();
BeanUtil.copyProperties(commentDO, commentVO);
Optional<UserDO> byId = userRepository.findById(commentDO.getCommentator());
byId.ifPresent(userDO -> {
commentVO.setCommentatorAvatarUrl(userDO.getAvatarUrl());
commentVO.setCommentatorName(userDO.getName());
});
commentVO.setGmtCreate(BlogUtil.formatDate(commentDO.getGmtCreate(), DATE_PATTERN));
commentVO.setGmtUpdate(BlogUtil.formatDate(commentDO.getGmtUpdate(), DATE_PATTERN));
commentVOS.add(commentVO);
});
return commentVOS;
}
}
- 编写Controller
package com.qianyucc.blog.controller.comm;
import com.qianyucc.blog.model.dto.*;
import com.qianyucc.blog.model.vo.*;
import com.qianyucc.blog.service.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* @author lijing
* @date 2019-10-13 12:19
* @description 与评论相关的api
*/
@RestController
@RequestMapping("/api/comm/comment")
public class CommentController {
@Autowired
private CommentService commentService;
@PostMapping("/insComment")
public RespDataVO comment(@RequestBody CommentDTO commentDTO) {
commentService.insComment(commentDTO);
return RespDataVO.ok("评论成功!");
}
@GetMapping("/getComments")
public List<CommentVO> getAllComments(Long articleId) {
List<CommentVO> commentVOS = commentService.findCommentByArticleId(articleId);
return commentVOS;
}
}
前端数据渲染
- 在
/src/request/api/url.js
中添加与评论相关的url
- 新建
/src/request/api/comment.js
封装请求api
import url from '@/request/api/url'
// 导入axios实例
import axios from '@/request/http'
export default {
submitComment(commentInfo, callback) {
axios
.post(url.doCommentUrl, commentInfo)
.then(callback)
.catch(err => {
console.log('submitComment Error');
})
},
getAllComments(articleId, callback) {
axios
.get(url.getCommentsUrl, {
params: {
articleId: articleId
}
})
.then(callback)
.catch(err => {
console.log("getAllComments Error");
})
}
}
-
别忘了在
/src/request/api/index.js
中导出 -
在
articleDetails.vue
中定义数据存储评论信息,并导入store
中的isLogin
和userInfo
data() {
return {
// 文章信息
article: {},
// 所有评论
comments: [],
// 文章内容
content: null,
// 与二级评论内容绑定
slcContent: null,
// 与一级评论内容绑定
commentContent: null
};
},
computed: {
...mapState({
isLogin: state => state.app.isLogin,
userInfo: state => state.user.userInfo
})
},
- 定义获取所有评论的方法
getAllComments() {
this.$api.comment.getAllComments(this.article.id, resp => {
this.comments = resp.data;
});
}
- 定义提交一级评论和提交二级评论的方法,注意每一次提交评论之后都要重新刷新一次评论列表
submitSlComment(id) {
if (this.slcContent == null || this.slcContent == "") {
return;
}
this.$api.comment.submitComment(
{
content: this.slcContent,
type: 2,
parentId: id,
commentator: this.isLogin ? this.userInfo.id : 0
},
resp => {
this.slcContent = "";
// 重新获取所有评论
this.getAllComments();
}
);
},
submitComment() {
if (!this.commentContent || this.commentContent == "") {
return;
}
this.$api.comment.submitComment(
{
content: this.commentContent,
type: 1,
parentId: this.article.id,
commentator: this.isLogin ? this.userInfo.id : 0
},
resp => {
this.commentContent = "";
// 重新获取所有评论
this.getAllComments();
}
);
}
}
- 在页面的
created()
方法中获取文章信息之后再获取该文章的所有评论
created() {
this.$api.article.getArticleById(this.$route.params.articleId, resp => {
this.article = resp.data;
let converter = new showdown.Converter({
// 使代码高亮显示
extensions: [showdownHighlight],
// 启用后可以为图片设置尺寸
parseImgDimensions: true
});
// markdown 转 html
this.content = converter.makeHtml(this.article.content);
// 获取所有评论,因为axios为异步操作,下面的操作不能写在该函数的外面
this.getAllComments();
});
}
- 将数据渲染到页面上:
<template>
<b-container class="main">
<!-- 文章标题 -->
<h2 class="title">{{article.title}}</h2>
<!-- 文章描述 -->
<h6 class="description">
<b-badge variant="info">{{article.type==1 ? '原创' : '转载'}}</b-badge>
<i class="icon iconfont icon-riqi"></i>
{{article.gmtUpdate}}
<i
class="icon iconfont icon-gaojian-zuozhe"
></i>
{{article.author}}
<i class="icon iconfont icon-yuedu"></i>
{{article.views}}
<i class="icon iconfont icon-fenlei"></i>
{{article.category}}
</h6>
<!-- 文章内容 -->
<div v-html="content"></div>
<!-- 标签 -->
<div class="tag-box">
<b-link class="tag" variant="info" v-for="(tag,index) in article.tags" :key="index">
<i class="icon iconfont icon-tag"></i>
{{tag}}
</b-link>
</div>
<!-- 评论回复 -->
<hr />
<h5>总共有{{comments.length}}条评论</h5>
<hr />
<!-- 一级评论列表 start -->
<b-media v-for="comment in comments" :key="comment.id">
<template v-slot:aside>
<b-img
width="60"
height="60"
:src="comment.commentator==0 ? '/static/images/no-name.png' : comment.commentatorAvatarUrl"
></b-img>
</template>
<h6
thumbnail
class="commentator-name"
v-text="comment.commentator==0 ? '匿名用户' : comment.commentatorName"
></h6>
<p>{{comment.content}}</p>
<p>
<i class="icon iconfont icon-dianzan1 link-icon"></i>
<i
class="icon iconfont icon-pinglun link-icon"
@click="showOrHideSecondLevelComments(comment.id)"
></i>
{{comment.secondLevelComments.length}}
</p>
<div :id="'second-level-comment-'+comment.id" style="display:none;">
<!-- 二级评论列表 start -->
<b-media v-for="cll in comment.secondLevelComments" :key="cll.id">
<template v-slot:aside>
<b-img
width="60"
height="60"
:src="cll.commentator==0 ? '/static/images/no-name.png' : cll.commentatorAvatarUrl"
></b-img>
</template>
<h6
thumbnail
class="commentator-name"
v-text="cll.commentator==0 ? '匿名用户' : cll.commentatorName"
></h6>
<p>{{cll.content}}</p>
</b-media>
<!-- 二级评论 end -->
<hr />
<b-textarea placeholder="请输入评论内容......" v-model="slcContent"></b-textarea>
<b-button class="pull-right" variant="success" @click="submitSlComment(comment.id)">提交</b-button>
</div>
</b-media>
<!-- 一级评论列表 end -->
<hr />
<b-media>
<template v-slot:aside>
<b-img
width="60"
height="60"
:src="isLogin ? userInfo.avatarUrl : '/static/images/no-name.png'"
></b-img>
</template>
<h6 thumbnail class="commentator-name" v-text="isLogin ? userInfo.name : '匿名用户'"></h6>
</b-media>
<b-textarea placeholder="请输入评论内容......" v-model="commentContent"></b-textarea>
<b-button variant="success" class="pull-right" @click="submitComment()">提交</b-button>
<!-- 回顶部按钮 -->
<i class="icon iconfont icon-huidingbu" @click="backTop()"></i>
</b-container>
</template>
- 效果如下:
更多推荐
已为社区贡献11条内容
所有评论(0)