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`, `
·
数据库表
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>
更多推荐
已为社区贡献1条内容
所有评论(0)