基于Spring Boot+Vue的博客系统 13——评论功能的实现

废弃说明:
这个专栏的文章本意是记录笔者第一次搭建博客的过程,文章里里有很多地方的写法都不太恰当,现在已经废弃,关于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里面定义根据parentIdtype字段查询的方法

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
    添加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中的isLoginuserInfo

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>&nbsp;&nbsp;
      <i class="icon iconfont icon-riqi"></i>
      &nbsp;{{article.gmtUpdate}}
      &nbsp;&nbsp;
      <i
        class="icon iconfont icon-gaojian-zuozhe"
      ></i>
      &nbsp;{{article.author}}
      &nbsp;&nbsp;
      <i class="icon iconfont icon-yuedu"></i>
      &nbsp;{{article.views}}
      &nbsp;&nbsp;
      <i class="icon iconfont icon-fenlei"></i>
      &nbsp;{{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>
  • 效果如下:
    评论效果演示
  • 9
    点赞
  • 62
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值