个人博客实现两级评论回复功能

博客源码地址 https://gitee.com/manster1231/master-blog
前端采用jQuery+SemanticUI,后端采用springboot+mp+thymeleaf

评论功能

这里采用个人认为较合适的两级评论来实现,示例如下
show
submit

1、评论提交与回复

主要是为了得到评论信息,和提交评论信息

1.数据库设计

这里就是简单的设计,只是用了一张表,大家也可以分为两张表来进行多表联查

FieldTypeComment
idbigint NOT NULLid
nicknamevarchar(255) NULL昵称
emailvarchar(255) NULL邮箱
contentvarchar(255) NULL内容
avatarvarchar(255) NULL头像
create_timedatetime NULL创建时间
blog_idbigint NULL所属博客id
parent_comment_idbigint NULL父评论id
admin_commenttinyint(1) NULL管理员评论
CREATE TABLE `t_comment` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `nickname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '昵称',
  `email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '邮箱',
  `content` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '内容',
  `avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '头像',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `blog_id` bigint DEFAULT NULL COMMENT '所属博客id',
  `parent_comment_id` bigint DEFAULT NULL COMMENT '父评论id',
  `admin_comment` tinyint(1) DEFAULT NULL COMMENT '管理员评论',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT

2.前端逻辑

1.评论流程图

输入信息
不为空
回调函数
为空
blog.html
点击发布调用绑定点击函数
验证内容
postData提交到后台
渲染页面,滚动到评论区,清理表单
弹出模态框

2.评论代码

这里回调渲染时是利用 thymeleafth:fragment="commentList"

<div id="comment-form" class="ui form">
    <input type="hidden" name="blogId" th:value="${blog.id}" />
    <input type="hidden" name="parentCommentId" value="-1" />
    <div class="field">
        <textarea name="content" placeholder="请输入评论信息..."></textarea>
    </div>
    <div class="fields">
        <div class="field m-mobile-wide m-margin-bottom-small">
            <div class="ui left icon input">
                <i class="user icon"></i>
                <input type="text" name="nickname" placeholder="姓名">
            </div>
        </div>
        <div class="field m-mobile-wide m-margin-bottom-small">
            <div class="ui left icon input">
                <i class="mail icon"></i>
                <input type="text" name="email" placeholder="邮箱">
            </div>
        </div>
        <div class="field m-margin-bottom-small m-mobile-wide">
            <button id="comment-btn" type="button" class="ui violet button m-mobile-wide "><i class="edit icon"></i>发布</button>
        </div>
    </div>
</div>
			//form验证
			$('.ui.form').form({
				fields: {
					content: {
						identifier: 'content',
						rules: [{
							type: 'empty',
							prompt: '请输入你的评论'
						}]
					},
					nickname: {
						identifier: 'nickname',
						rules: [{
							type: 'empty',
							prompt: '请输入你的昵称'
						}]
					},
					email: {
						identifier: 'email',
						rules: [{
							type: 'empty',
							prompt: '请填写正确的邮箱地址'
						}]
					}
				}
			});

			//得到评论列表
			$(function () {
                //这里的${blog.id}来自于点击查看博客后BlogController中传来的值
				$("#comment-container").load(/*[[@{/comments/{id}(id=${blog.id})}]]*/"comments/6");
			});

			//发送评论请求
			$('#comment-btn').click(function () {
				let boo = $('.ui.form').form('validate form');
				if(boo){
					postData();
					console.log('校验成功');
				} else {
					$('.ui.basic.modal').modal('show');
				}
			});
			//评论提交到后端
			function postData() {
				$("#comment-container").load(/*[[@{/comments}]]*/"",{
					"parentCommentId" : $("[name='parentCommentId']").val(),
					"blogId" : $("[name='blogId']").val(),
					"nickname" : $("[name='email']").val(),
					"content" : $("[name='content']").val()
				},function (responseTxt, statusTxt, xhr) {
					//清理
					clearContent();

				});
			}
			//提交成功后清理评论区内容
			function clearContent() {
				$("[name='content']").val('');
				$("[name='parentCommentId']").val(-1);
				$("[name='content']").attr("placeholder", "请输入评论信息...");
			}

3.回复流程图

表单添加这个人的昵称和父评论id
不为空
得到数据进行渲染和清理
为空
blog.html
点击回复
滚动到评论表单
点击发布
内容都不为空
ajax提交到后台
滚动到评论列表
弹出模态框

4.回复代码

<div id="comment-container" class="ui teal segment">
    <div th:fragment="commentList">
        <div class="ui threaded comments" style="max-width: 100%">
            <h3 class="ui dividing header">评论</h3>
            <div class="comment">
                <a class="avatar">
                    <img src="https://picsum.photos/id/237/200/200">
                </a>
                <div class="content">
                    <a class="author">Matt</a>
                    <div class="metadata">
                        <span class="date">Today at 5:42PM</span>
                    </div>
                    <div class="text">
                        How artistic!
                    </div>
                    <div class="actions">
                        <a class="reply" data-commentid="1" data-commentnickname="Matt" onclick="reply(this)">回复</a>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
			//回复
			function reply(obj) {
				let commentId = $(obj).data('commentid');
				let commentNickname = $(obj).data('commentnickname');
				//添加信息到评论表单
				$("[name='content']").attr("placeholder", "@" + commentNickname).focus();
				$("[name='parentCommentId']").val(commentId);
				//滚动到评论表单
				$(window).scrollTo($('#comment-form'),500);
			}

3.后端逻辑

  • 首先,当页面加载时将该篇博客的 id 用ajax传到后台,用来查询出关于该篇博客的所有评论
  • 然后,前端局部渲染 th:fragment="commentList"
  • 当评论时
    • 会给予该评论一个隐藏域 parentCommentId 父评论id
    • 然后提交到后台,重定向到查询列表页面
    • 再次查询出评论列表渲染页面
  • 当回复时,我们点击回复会将该条评论的id作为 新评论的 parentCommentId 父评论id
    • 后续步骤与评论一样了

controller

package com.manster.controller;

import com.manster.pojo.Comment;
import com.manster.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

/**
 * @Author manster
 * @Date 2021/5/3
 **/
@Controller
public class CommentController {

    @Autowired
    private CommentService commentService;

    @Value("${project.avatar}")
    private String avatar;

    /**
     * 得到列表
     * @param blogId 评论所属博客id
     * @param model 评论数据
     * @return 返回渲染fragment
     */
    @GetMapping("/comments/{blogId}")
    public String comments(@PathVariable Long blogId, Model model){
        model.addAttribute("comments", commentService.listCommentByBlogId(blogId));
        return "blog :: commentList";
    }

    @PostMapping("/comments")
    public String post(Comment comment){
        commentService.saveComment(comment, avatar);
        return "redirect:/comments/" + comment.getBlogId();
    }

}

service

@Autowired
private CommentMapper commentMapper;

@Override
public List<Comment> listCommentByBlogId(Long blogId) {
    List<Comment> comments = commentMapper.selectList(new QueryWrapper<Comment>().eq("blog_id", blogId).orderByAsc("create_time"));
    return CommentReplyUtils.commentReply(comments);
}

@Override
public int saveComment(Comment comment, String avatar) {
    comment.setCreateTime(LocalDateTime.now());
    comment.setAvatar(avatar);
    return commentMapper.insert(comment);
}

2、评论信息列表展示

主要是为了将评论更加有条理的展示出来

主要麻烦在了找出父子两层的关系,我就用最粗暴的方式,以最简单的思维先做出来

package com.manster.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.manster.mapper.CommentMapper;
import com.manster.pojo.Comment;
import com.manster.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author manster
 * @Date 2021/5/3
 **/
@Service
public class CommentServiceImpl implements CommentService {

    @Autowired
    private CommentMapper commentMapper;

    @Override
    public List<Comment> listCommentByBlogId(Long blogId) {
        List<Comment> comments = commentMapper.selectList(new QueryWrapper<Comment>().eq("blog_id", blogId).orderByAsc("create_time"));
        return firstComment(comments);
    }

    @Override
    public int saveComment(Comment comment, String avatar) {
        comment.setCreateTime(LocalDateTime.now());
        comment.setAvatar(avatar);
        return commentMapper.insert(comment);
    }

    public List<Comment> firstComment(List<Comment> comments){
        //存储父评论为根评论-1的评论
        ArrayList<Comment> list = new ArrayList<>();
        for (Comment comment : comments) {
            //其父id小于0则为第一级别的评论
            if(comment.getParentCommentId() < 0){
                //我们将该评论下的所有评论都查出来
                comment.setReplyComments(findReply(comments,comment.getId()));
                //这就是我们最终数组中的Comment
                list.add(comment);
            }
        }
        return list;
    }

    /**
     * @param comments 我们所有的该博客下的评论
     * @param targetId 我们要查到的目标父id
     * @return 返回该评论下的所有评论
     */
    public List<Comment> findReply(List<Comment> comments, Long targetId){
        //第一级别评论的子评论集合
        ArrayList<Comment> reply = new ArrayList<>();
        for (Comment comment : comments) {
            //发现该评论的父id为targetId就将这个评论加入子评论集合
            if(find(comment.getParentCommentId(),targetId)){
                reply.add(comment);
            }
        }
        return reply;
    }

    public boolean find(Long id, Long target){
        //不将第一节评论本身加入自身的子评论集合
        if(id < 0){
            return false;
        }
        //如果父id等于target,那么该评论就是id为target评论的子评论
        if (id.equals(target)){
            return true;
        } else{
            //否则就再向上找
            return find(commentMapper.selectById(id).getParentCommentId(),target);
        }
    }

}

后端查出来的是两层结点,父 comment 中有一个 List<Comment> replyListcomment 的集合,所有我们需要两层来遍历这种结果

<div class="comment" th:each="comment : ${comments}">
    <a class="avatar">
        <img src="https://picsum.photos/id/237/200/200" th:src="@{${comment.avatar}}">
    </a>
    <div class="content">
        <a class="author" th:text="${comment.nickname}">Matt</a>
        <div class="metadata">
            <span class="date" th:text="${#temporals.format(comment.createTime, 'yyyy-MM-dd HH:mm')}">Today at 5:42PM</span>
        </div>
        <div class="text" th:text="${comment.content}">
            How artistic!
        </div>
        <div class="actions">
            <a class="reply" data-commentid="1" data-commentnickname="Matt" th:attr="data-commentid=${comment.id}, data-commentnickname=${comment.nickname}" onclick="reply(this)">回复</a>
        </div>
    </div>
    <div class="comments" th:if="${#arrays.length(comment.replyComments)} gt 0">
        <div class="comment" th:each="reply : ${comment.replyComments}">
            <a class="avatar">
                <img src="https://picsum.photos/id/237/200/200" th:src="@{${reply.avatar}}">
            </a>
            <div class="content">
                <a class="author">
                    <span  th:text="${reply.nickname}">Matt</span>&nbsp;<span th:text="|@ ${comment.nickname}|" class="m-grey">@jeny</span>
                </a>
                <div class="metadata">
                    <span class="date" th:text="${#temporals.format(reply.createTime, 'yyyy-MM-dd HH:mm')}">Today at 5:42PM</span>
                </div>
                <div class="text" th:text="${reply.content}">
                    How artistic!
                </div>
                <div class="actions">
                    <a class="reply" data-commentid="1" data-commentnickname="Matt" th:attr="data-commentid=${reply.id}, data-commentnickname=${reply.nickname}" onclick="reply(this)">回复</a>
                </div>
            </div>
        </div>
    </div>
</div>

3、管理员回复评论功能

因为我将后台管理和前台访问的服务放在两个不同的端口上,这样别人就没那么容易访问到我的后台。所以管理员回复评论就是简单的验证一下邮箱是不是管理员的(昵称大家都能看见,但是邮箱没有展示)

1.后台

controller

    @Value("${project.email}")
    private String email;
    @Value("${project.adminAvatar}")
    private String adminAvatar;    

	@PostMapping("/comments")
    public String post(Comment comment){
        if (email.equals(comment.getEmail())) {
            comment.setAdminComment(true);
            commentService.saveComment(comment, adminAvatar);
        } else {
            commentService.saveComment(comment, avatar);
        }
        return "redirect:/comments/" + comment.getBlogId();
    }

2.前端

<a class="ui teal mini tag label m-padded-tb-tiny" th:if="${reply.adminComment}">博主</a>&nbsp;
  • 6
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值