springboot实现多级评论功能

数据库表

在这里插入图片描述

create table `t_comment` (
	`id` int (11),
	`content` varchar (765),
	`user_id` int (11),
	`time` varchar (150),
	`pid` int (11),
	`origin_id` int (11),
	`article_id` int (11)
); 
insert into `t_comment` (`id`, `content`, `user_id`, `time`, `pid`, `origin_id`, `article_id`) values('-1749151742','888','28','2022-03-27 16:10:29','1396666369','12546050','-998318078');
insert into `t_comment` (`id`, `content`, `user_id`, `time`, `pid`, `origin_id`, `article_id`) values('-1707094014','11','1','2022-03-26 22:06:54','-1228943358','-1228943358','-998318078');
insert into `t_comment` (`id`, `content`, `user_id`, `time`, `pid`, `origin_id`, `article_id`) values('-1228943358','444','1','2022-03-26 21:38:03','-171978750','-171978750','-998318078');
insert into `t_comment` (`id`, `content`, `user_id`, `time`, `pid`, `origin_id`, `article_id`) values('-339865599','ddd','1','2022-03-27 16:02:13','1396666369','12546050','-998318078');
insert into `t_comment` (`id`, `content`, `user_id`, `time`, `pid`, `origin_id`, `article_id`) values('-318779391','等','1','2022-03-26 21:41:34','-1228943358','-1228943358','-998318078');
insert into `t_comment` (`id`, `content`, `user_id`, `time`, `pid`, `origin_id`, `article_id`) values('-226504702','1111','1','2022-03-26 21:40:12','-1228943358','-1228943358','-998318078');
insert into `t_comment` (`id`, `content`, `user_id`, `time`, `pid`, `origin_id`, `article_id`) values('12546050','111','1','2022-03-27 15:26:18',NULL,NULL,'-998318078');
insert into `t_comment` (`id`, `content`, `user_id`, `time`, `pid`, `origin_id`, `article_id`) values('79679489','1','1','2022-03-26 21:42:20','-1228943358','-1228943358','-998318078');
insert into `t_comment` (`id`, `content`, `user_id`, `time`, `pid`, `origin_id`, `article_id`) values('1396666369','222','1','2022-03-27 15:26:22','12546050','12546050','-998318078');
insert into `t_comment` (`id`, `content`, `user_id`, `time`, `pid`, `origin_id`, `article_id`) values('1652518913','222','1','2022-03-27 15:26:30','1396666369','1396666369','-998318078');

CommentController.java

package com.qingge.springboot.controller;

import cn.hutool.core.date.DateUtil;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelReader;
import cn.hutool.poi.excel.ExcelWriter;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletOutputStream;
import java.io.InputStream;
import java.net.URLEncoder;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.qingge.springboot.common.Result;
import org.springframework.web.multipart.MultipartFile;
import com.qingge.springboot.entity.User;
import com.qingge.springboot.utils.TokenUtils;

import com.qingge.springboot.service.ICommentService;
import com.qingge.springboot.entity.Comment;

import org.springframework.web.bind.annotation.RestController;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author 
 * @since 2022-05-04
 */
@RestController
@RequestMapping("/comment")
public class CommentController {

    @Resource
    private ICommentService commentService;

    private final String now = DateUtil.now();

    // 新增或者更新
    @PostMapping
    public Result save(@RequestBody Comment comment) {
        if (comment.getId() == null){
            comment.setUserId(TokenUtils.getCurrentUser().getId());
            comment.setTime(DateUtil.now());
            if (comment.getPid() != null){
//                判断如果是回复进行处理
//                找到评论的父id
                Integer pid = comment.getPid();
//                        找到父评论
                Comment pComment= commentService.getById(pid);
//                如果存在当前父评论的祖宗
                if (pComment.getOriginId() != null){//如果当前回复的父级有祖宗,那么就设置相同的祖宗
//                将父评论的祖宗id赋值给当前评论的祖宗id
                    comment.setOriginId(pComment.getOriginId());
                }else {
                    //否则就将父评论id设置为当前评论的祖宗id
                    comment.setOriginId(comment.getPid());
                }

            }
        }
        return Result.success( commentService.saveOrUpdate(comment));
    }

    @DeleteMapping("/{id}")
    public Result delete(@PathVariable Integer id) {
        commentService.removeById(id);
        return Result.success();
    }

    @PostMapping("/del/batch")
    public Result deleteBatch(@RequestBody List<Integer> ids) {
        commentService.removeByIds(ids);
        return Result.success();
    }

    @GetMapping
    public Result findAll() {
        return Result.success(commentService.list());
    }
    @GetMapping("/tree/{articleId}")
    public Result findTree(@PathVariable Integer articleId){
        List<Comment> articleComments= commentService.findCommentDetail(articleId);//根据文章id查询所有的评论和回复数据
//        查询评论(不包括回复) 过滤得到祖宗id为空的评论
        List<Comment> originList = articleComments.stream().filter(comment -> comment.getOriginId() == null).collect(Collectors.toList());//表示回复对象
//        设置评论数据的子节点,也就是回复内容
        for (Comment origin : originList) {
//                过滤得到回复的祖宗id等于评论的id
            List<Comment> comments = articleComments.stream().filter(comment -> origin.getId().equals(comment.getOriginId())).collect(Collectors.toList());
            System.out.println("============================");
            for (Comment comment : comments) {
                System.out.println(comment);
            }
            System.out.println("==============================");
            comments.forEach(comment -> {
//                如果存在回复的父id,给回复设置其父评论的用户id和用户昵称,这样评论就有能@的人的用户id和昵称
//                v相当于过滤得到的父评论对象
                Optional<Comment> pComment = articleComments.stream().filter(c1 -> c1.getId().equals(comment.getPid())).findFirst();
                pComment.ifPresent((v ->{
//                                找到父级评论的用户id和用户昵称,并设置当前的回复对象
                    comment.setPUserId(v.getUserId());
                    comment.setPNickname(v.getNickname());
                }));
            });
            origin.setChildren(comments);
        }
        return Result.success(originList);
    }
    @GetMapping("/{id}")
    public Result findOne(@PathVariable Integer id) {
        return Result.success(commentService.getById(id));
    }

    @GetMapping("/page")
    public Result findPage(@RequestParam(defaultValue = "") String name,
                           @RequestParam Integer pageNum,
                           @RequestParam Integer pageSize) {
        QueryWrapper<Comment> queryWrapper = new QueryWrapper<>();
        queryWrapper.orderByDesc("id");
        if (!"".equals(name)) {
            queryWrapper.like("name", name);
        }
        return Result.success(commentService.page(new Page<>(pageNum, pageSize), queryWrapper));
    }
    private User getUser() {
        return TokenUtils.getCurrentUser();
    }

}

comment.java

package com.qingge.springboot.entity;

import java.io.Serializable;
import java.util.List;

import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;

/**
 * <p>
 * 
 * </p>
 *
 * @author 
 * @since 2022-05-04
 */
@Getter
@Setter
@ApiModel(value = "Comment对象", description = "")
public class Comment implements Serializable {

    private static final long serialVersionUID = 1L;

    private Integer id;

    @ApiModelProperty("内容")
    private String content;

    @ApiModelProperty("评论人id")
    private Integer userId;

    @ApiModelProperty("评论时间")
    private String time;

    @ApiModelProperty("父id")
    private Integer pid;

    @ApiModelProperty("最上级评论id")
    private Integer originId;
    @TableField(exist = false)
    private String pNickname;//父节点的用户昵称
    @TableField(exist = false)
    private Integer pUserId;  // 父节点的用户id
    @ApiModelProperty("关联文章的id")
    private Integer articleId;
    @TableField(exist = false)
    private String nickname;
    @TableField(exist = false)
    private String avatarUrl;

    @TableField(exist = false)
    private List<Comment> children;

}

Article.vue

<template>
  <div style="color: #666666">
    <div style="margin: 10px 0">
      <el-input style="width: 300px" placeholder="请输入文章标题" suffix-icon="el-icon-search" v-model="name" size="small"></el-input>
      <el-button class="ml-5" type="primary" @click="load" size="small">搜索</el-button>
      <el-button type="warning" @click="reset" size="small">重置</el-button>
    </div>
    <div style="margin: 10px 0">
      <div style="padding: 10px 0;border-bottom: 1px dashed #ccc" v-for="item in tableData" :key="item.id">
        <div class="pd-10" style="font-size: 20px;color: #3F5EFB;cursor: pointer" @click="$router.push('/front/articleDetail?id='+item.id)">{{item.title}}</div>
<!--        <img :src=item.firstPicture class="image" v-if="item.firstPicture">-->
        <div>
        <el-image v-if="item.firstPicture" :src="item.firstPicture"
        style="width: 200px;height: 200px">
        </el-image>
        </div>
        <div style="font-size: 14px;margin-bottom: 10px">
          <span>{{item.description}}</span>
        </div>
        <div style="font-size: 10px;margin-top: 10px">
          <i class="el-icon-user-solid"></i>
          <span>{{item.userName}}</span>
          <i class="el-icon-time" style="margin-left: 10px"></i>
          <span>{{item.createTime}}</span>
          <i class="el-icon-s-opportunity" style="margin-left: 10px"></i>
          <span>分类:{{item.typeName}}</span>
          <i style="margin-left: 10px"></i>
          <span>字数:{{item.words}}</span>
        </div>
      </div>
    </div>
    <div style="padding: 10px 0">
      <el-pagination
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
          :current-page="pageNum"
          :page-sizes="[2, 5, 10, 20]"
          :page-size="pageSize"
          layout="total, prev, pager, next"
          :total="total">
      </el-pagination>
    </div>
  </div>
</template>

<script>

import axios from "axios";

export default {
  name: "Article",
  data() {
    return {
      form: {},
      tableData: [],
      name: '',
      multipleSelection: [],
      pageNum: 1,
      pageSize: 10,
      total: 0,
      dialogFormVisible: false,
      teachers: [],
      user: localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : {},
      content: '',
      viewDialogVis: false,
      categoryName: ''
    }
  },
  created() {
    this.load()
  },
  methods: {
    view(content){
      this.content = content
      this.viewDialogVis = true
    },
    // 绑定@imgAdd event
    imgAdd(pos, $file) {
      let $vm = this.$refs.md
      // 第一步.将图片上传到服务器.
      const formData = new FormData();
      formData.append('file', $file);
      axios({
        url: 'http://localhost:9090/file/upload',
        method: 'post',
        data: formData,
        headers: {'Content-Type': 'multipart/form-data'},
      }).then((res) => {
        // 第二步.将返回的url替换到文本原位置![...](./0) -> ![...](url)
        $vm.$img2Url(pos, res.data);
      })
    },
    load() {
      this.request.get("/blog/page", {
        params: {
          pageNum: this.pageNum,
          pageSize: this.pageSize,
          name: this.name,
        }
      }).then(res => {

        this.tableData = res.data.records
        this.total = res.data.total

      })
    },
    changeEnable(row) {
      this.request.post("/blog/update", row).then(res => {
        if (res) {
          this.$message.success("操作成功")
        }
      })
    },
    reset() {
      this.name = ""
      this.load()
    },
    handleSizeChange(pageSize) {
      console.log(pageSize)
      this.pageSize = pageSize
      this.load()
    },
    handleCurrentChange(pageNum) {
      console.log(pageNum)
      this.pageNum = pageNum
      this.load()
    },
    download(url) {
      window.open(url)
    },
  }
}
</script>

<style scoped>
.image {
  width: 100%;
  height: auto;
}
</style>

ArticleDetail.vue

<template>
  <div style="color: #666">
    <div style="margin: 20px 0; ">
      <div class="pd-10" style="font-size: 20px; color: #3F5EFB; cursor: pointer">{{ article.title }}</div>
      <div style="font-size: 14px; margin-top: 10px">
        <i class="el-icon-user-solid"></i> <span>{{ article.userName }}</span>
        <i class="el-icon-time" style="margin-left: 10px"></i> <span>{{ article.createTime }}</span>
        <i class="el-icon-s-opportunity" style="margin-left: 10px"></i><span>分类:{{article.typeName}}</span>
        <i style="margin-left: 10px"></i><span>字数:{{article.words}}</span>
      </div>
    </div>

    <div style="margin: 20px 0">
      <mavon-editor
          class="md"
          :value="article.content"
          :subfield="false"
          :defaultOpen="'preview'"
          :toolbarsFlag="false"
          :editable="false"
          :scrollStyle="true"
          :ishljs="true"
      />
    </div>

    <div style="margin: 30px 0">
      <div style="margin: 10px 0">
        <div style="border-bottom: 1px solid orangered; padding: 10px 0; font-size: 20px">评论</div>
        <div style="padding: 10px 0">
          <el-input size="small" type="textarea" v-model="commentForm.content"></el-input>
        </div>
        <div class="pd-10" style="text-align: right">
          <el-button type="primary" size="small" @click="save">评论</el-button>
        </div>
      </div>

      <!--      评论列表-->
      <div>
        <div v-for="item in comments" :key="item.id" style="border-bottom: 1px solid #ccc; padding: 10px 0; ">
          <div style="display: flex">
            <div style="width: 100px; text-align: center">
              <el-image :src="item.avatarUrl" style="width: 50px; height: 50px; border-radius: 50%"></el-image>
            </div> <!--  头像-->
            <div style="flex: 1; font-size: 14px; padding: 5px 0; line-height: 25px">
              <b>{{ item.nickname }}</b>
              <span>{{ item.content }}</span>

              <div style="display: flex; line-height: 20px; margin-top: 5px">
                <div style="width: 200px;">
                  <i class="el-icon-time"></i><span style="margin-left: 5px">{{ item.time }}</span>
                </div>
                <div style="text-align: right; flex: 1">
                  <el-button style="margin-left: 5px" type="text" @click="handleReply(item.id)">回复</el-button>
                  <el-button type="text" style="color: red" @click="del(item.id)" v-if="user.id === item.userId">删除</el-button>
                </div>
              </div>
            </div>   <!--  内容-->
          </div>

          <div v-if="item.children.length"  style="padding-left: 200px;">
            <div v-for="subItem in item.children" :key="subItem.id"  style="background-color: #f0f0f0; padding: 5px 20px">
              <!--          回复列表-->
              <div style="font-size: 14px; padding: 5px 0; line-height: 25px">
                <div>
                  <b style="color: #3a8ee6" v-if="subItem.pnickname">@{{ subItem.pnickname }}</b>
                </div>
                <div style="padding-left: 5px">
                  <b>{{ subItem.nickname }}</b>
                  <span>{{ subItem.content }}</span>
                </div>

                <div style="display: flex; line-height: 20px; margin-top: 5px; padding-left: 5px">
                  <div style="width: 200px;">
                    <i class="el-icon-time"></i><span style="margin-left: 5px">{{ subItem.time }}</span>
                  </div>
                  <div style="text-align: right; flex: 1">
                    <el-button style="margin-left: 5px" type="text" @click="handleReply(subItem.id)">回复</el-button>
                    <el-button type="text" style="color: red" @click="del(subItem.id)" v-if="user.id === subItem.userId">删除</el-button>
                  </div>
                </div>
              </div>   <!--  内容-->
            </div>
          </div>
        </div>
      </div>
    </div>
    <el-dialog title="回复" :visible.sync="dialogFormVisible" width="30%" >
      <el-form label-width="80px" size="small">
        <el-form-item label="回复内容">
          <el-input type="textarea" v-model="commentForm.contentReply" autocomplete="off"></el-input>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogFormVisible = false" size="small">取 消</el-button>
        <el-button type="primary" @click="save" size="small">确 定</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
export default {
  name: "ArticleDetail",
  data() {
    return {
      article: {},
      user: localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : {},
      id: this.$route.query.id,
      comments: [],
      commentForm: {},
      dialogFormVisible: false
    }
  },
  created() {
    this.load()
    this.loadComment()
  },
  methods: {
    handleReply(pid){
      this.commentForm = {pid: pid}
      this.dialogFormVisible = true
    },
    load() {
      this.request.get("/blog/"+ this.id).then(res => {
        this.article = res.data
      })
    },
    //加载评论
    loadComment(){
      this.request.get("/comment/tree/"+ this.id).then(res => {
        this.comments = res.data
      })
    },
    del(id) {
      this.request.delete("/comment/" + id).then(res => {
        if (res.code === '200') {
          this.$message.success("删除成功")
          this.loadComment()
        } else {
          this.$message.error("删除失败")
        }
      })
    },
    save() {
      if (!this.user.id){
        this.$message.warning("请登录后操作")
        return
      }
      //这个是为了防止回复框实时同步
      if (this.commentForm.contentReply){
        this.commentForm.content = this.commentForm.contentReply
      }
      this.commentForm.articleId = this.id
      this.request.post("/comment", this.commentForm).then(res => {
        if (res.code === '200') {
          this.$message.success("评论成功")
          this.commentForm = {} //初始化评论对象内容
          this.loadComment()
          this.dialogFormVisible = false
        } else {
          this.$message.error(res.msg)
        }
      })
    }
  }
}
</script>

<style scoped>

</style>

  • 5
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙崎流河

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值