1. 前言
这是本人毕业设计中其中一个功能点,就是用户在微信小程序(使用uniapp框架)中的首页模块中可以对健身资讯进行评论或者社区模块对别人发布的动态进行评论。
2. 实现效果(最后放个演示视频吧)
3. 实现思路
(1)首先,在前端页面中编写好一个递归组件,然后在资讯详情模块引用。
为什么要搞个递归组件?
因为,这么多的评论它们肯定是一个树状结构,树状结构得用递归呈现
(2)第二步,就是需要在服务端编写一个接口,查询数据库中所有资讯评论把它们构造成一个树状结构了,我这里使用java语言编写逻辑代码
4. 实现代码
4.1 vue递归组件
(1)创建一个treeComment递归组件
当使用它时,只需要把父组件传来的数据通过props的方式传进去即可。
<template>
<view>
<u-toast ref="uToast" />
<!-- 显示父评论的所有子评论 -->
<view class="vfor" v-for="(item,index) in commentList" :key="index">
<view class="item_wrap">
<view class="left">
<view class="avatar">
<u-image width="58rpx" height="58rpx" shape="circle" :src="item.avatar"></u-image>
</view>
<view class="nick">
<text>{{item.nickname}}</text>
</view>
<view class="ptime">
<text>{{item.createTime}}</text>
</view>
</view>
</view>
<view class="content" @click="reply(item.nickname,item.id,item.uid)" >
<view class="reply_to">@{{item.nickname}}:</view>
<view class="reply_content">{{item.content}}</view>
</view>
<tree-comment :commentList="item.commentChildren" :nid="item.nid" :uid="uid"></tree-comment>
</view>
<u-mask :show="isInput" @click="maskClick">
<view class="commentInputView">
<view class="inputView">
<u-input v-model="wContent" clearable="false" :focus="focus" confirm-type="评论" @confirm="writePComment"
:placeholder="'回复'+nickname" />
</view>
</view>
</u-mask>
<u-action-sheet :list="myList" v-model="mySheetShow" :cancel-btn="true" @click="mySheetClick()"></u-action-sheet>
<u-action-sheet :list="otherList" v-model="otherSheetShow" :cancel-btn="true" @click="otherSheetClick()"></u-action-sheet>
</view>
</template>
<script>
import treeComment from '../../component/treeComment/treeComment.vue'
var time = require('../../../common/datetime.js');
export default {
components: {
treeComment
},
props: {
commentList: Array,
nid: String,
uid:String
},
data() {
return {
wContent: '',
isInput: 0,
nickname: '',
focus: false,
cid: '',
myList: [{
text: '回复'
}, {
text: '删除'
}],
otherList: [{
text: '回复'
}, {
text: '举报'
}],
mySheetShow: false,
otherSheetShow: false
}
},
methods: {
// 点击用户评论传递参数
reply(nickname, cid,uid) {
this.nickname = nickname;
this.cid = cid;
if (uid === this.uid) {
this.mySheetShow = true;
} else {
this.otherSheetShow = true;
}
},
// 点击用户评论弹出遮罩
maskClick() {
this.isInput = false;
this.focus = false;
},
// 回复父级评论
writePComment() {
this.$u.api.writeComment({
nid: this.nid,
pid: this.cid,
content: this.wContent
}).then(res => {
if (res.msg === '评论成功') {
this.wContent = '';
this.isInput = false;
this.$refs.uToast.show({
title: '评论成功',
type: 'success'
})
this.$emit('updatePage');
} else {
this.$refs.uToast.show({
title: '评论出错,请登录',
type: 'warning'
})
}
})
},
// 点击操作菜单
mySheetClick(index){
if(index===0){
this.isInput = 1;
this.focus = 1;
}else if(index===1){
this.$u.api.deleteNewsCommentById({
id:this.cid
}).then(res => {
if(res.msg==='删除成功'){
this.$refs.uToast.show({
title: '删除成功',
type: 'success'
})
this.$emit('updatePage');
}
})
}
},
otherSheetClick(index){
if(index===0){
this.isInput = 1;
this.focus = 1;
}else if(index===1){
}
},
}
}
</script>
<style lang="scss" scoped>
.vfor {
.item_wrap {
display: flex;
justify-content: space-between;
margin-top: 20rpx;
.left {
display: flex;
align-items: center;
.avatar {}
.nick {
text {
font-weight: bold;
font-size: 37rpx;
margin-left: 20rpx;
}
}
.ptime {
text {
font-size: 27rpx;
margin-left: 10rpx;
color: #949494;
}
}
}
}
.content {
margin-left: 82rpx;
font-size: 33rpx;
width: 580rpx;
display: flex;
align-items: center;
.reply_to{
color: #45aaf2;
margin-right: 10rpx;
}
.reply_content{
}
}
}
.commentInputView {
width: 750rpx;
height: 100rpx;
background-color: #fff;
border-top: 1rpx solid #eee;
position: fixed;
bottom: 0;
display: flex;
align-items: center;
.inputView {
margin: 30rpx 20rpx;
background-color: #f4f4f4;
height: 60rpx;
width: 650rpx;
border-radius: 30rpx;
padding-left: 50rpx !important;
}
}
</style>
(2)父组件使用递归组件
注意: 从热门评论开始看,资讯详情页面的样式我就不贴了,免得大家看得脑嗡嗡的,同时,有些方法我也删了,因为不是这篇文章要讲述的内容,如果你有什么疑惑可以在评论区留言 🍿🍿🍿
<template>
<view>
<u-toast ref="uToast" />
<view class="loading" v-if="!isShow">
<u-loading mode="circle" color="blue" size="29"></u-loading>
</view>
<view class="detail" v-if="isShow">
<view class="title">
{{news.title}}
</view>
<view class="icon">
<view class="time">
<u-icon name="clock-fill" color="#999aaa" size="35"></u-icon>
<text>{{news.createTime}}</text>
</view>
<view class="zan">
<u-icon name="thumb-up-fill" :color="status.like?'#ff6b6b':'#999aaa'" size="35" @click="likeNews"></u-icon>
<text>{{news.likes}}</text>
</view>
<view class="see">
<u-icon name="eye-fill" color="#999aaa" size="35"></u-icon>
<text>{{news.views}}</text>
</view>
<view class="star">
<u-icon name="star-fill" :color="status.collection?'#ff6b6b':'#999aaa'" size="35" @click="starNews"></u-icon>
<text>{{news.collections}}</text>
</view>
</view>
<view class="news">
<rich-text :nodes="replaceSpecialChar(news.detail)"></rich-text>
</view>
<view class="comment">
<view class="top">- 热门评论 -</view>
<view v-if="commentList.length === 0" class="noComment">
<text>暂无评论</text>
</view>
<!-- 父评论 -->
<view class="vfor" v-for="(item,index) in commentList" :key="index">
<view class="item_wrap">
<view class="left">
<view class="avatar">
<u-image width="58rpx" height="58rpx" shape="circle" :src="item.avatar"></u-image>
</view>
<view class="nick">
<text>{{item.nickname}}</text>
</view>
<view class="ptime">
<text>{{item.createTime}}</text>
</view>
</view>
<view class="right">
<view class="likes">
<text v-if="item.likes === 0 || item.likes === null"></text>
<text v-else>{{item.likes}}</text>
<u-icon name="thumb-up-fill" :color="status.commentLike[index]?'#ff6b6b':'#3d3d3d'" size="35" @click="likeComment(item.id,index)"></u-icon>
</view>
</view>
</view>
<view class="content" @click="reply(item.nickname,item.id,item.uid)">{{item.content}}</view>
<view class="reply" v-if="item.replies">
<!-- <view class="hasReply" v-if="!item.isShow" @click="showComment(item)"> -->
<view class="hasReply" @click="changeStatus(index)" v-if="!scopesDefault[index]">
<text>{{item.replies}}条回复</text>
<u-icon name="arrow-right" color="#999aaa" size="28"></u-icon>
</view>
<view v-if="scopesDefault[index]">
<view class="vfor2" v-for="(item2,index2) in item.commentChildren" :key="index2">
<view class="item_wrap2">
<view class="left2">
<view class="avatar2">
<u-image width="58rpx" height="58rpx" shape="circle" :src="item2.avatar"></u-image>
</view>
<view class="nick2">
<text>{{item2.nickname}}</text>
</view>
<view class="ptime2">
<text>{{item2.createTime}}</text>
</view>
</view>
</view>
<view class="content2" @click="reply(item2.nickname,item2.id,item2.uid)">
{{item2.content}}
</view>
<my-tree :commentList="item2.commentChildren" :nid="item2.nid" :uid="uid" @updatePage="updatePage"></my-tree>
</view>
</view>
</view>
</view>
</view>
<u-mask :show="isInput" @click="maskClick">
<view class="commentInputView">
<view class="inputView">
<u-input v-model="wContent" clearable="false" :focus="focus" confirm-type="评论" @confirm="writePComment"
:placeholder="'回复'+nickname" />
</view>
</view>
</u-mask>
<u-action-sheet :list="myList" v-model="mySheetShow" :cancel-btn="true" @click="mySheetClick()"></u-action-sheet>
<u-action-sheet :list="otherList" v-model="otherSheetShow" :cancel-btn="true" @click="otherSheetClick()"></u-action-sheet>
<view class="writeComment">
<view class="write_wrap">
<view class="write">
<u-input v-model="wContent" :clearable="false" confirm-type="评论" @confirm="writeComment" :type="text" placeholder="缺少你的神评..." />
</view>
<view class="w_zan">
<u-icon name="thumb-up-fill" :color="status.like?'#ff6b6b':'#3d3d3d'" size="38" @click="likeNews"></u-icon>
</view>
<view class="w_star">
<u-icon name="star-fill" :color="status.collection?'#ff6b6b':'#3d3d3d'" size="38" @click="starNews"></u-icon>
</view>
<view class="w_share">
<button open-type="share">
<u-icon name="zhuanfa" color="#3d3d3d" size="38"></u-icon>
</button>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import myTree from '../component/treeComment/treeComment.vue';
var time = require('../../common/datetime.js');
var timer = null,
_self;
export default {
components: {
myTree
},
data() {
return {
id: '',
uid: '',
news: '',
city: '',
isShow: false,
wContent: '',
commentList: [],
status: {},
isInput: 0,
nickname: '',
focus: false,
cid: '',
scopesDefault: [],
scopes: [],
myList: [{
text: '回复'
}, {
text: '删除'
}],
otherList: [{
text: '回复'
}, {
text: '举报'
}],
mySheetShow: false,
otherSheetShow: false
}
},
onLoad(options) {
this.id = options.id;
this.uid = uni.getStorageSync('uid');
_self = this;
this.$u.api.getFUser().then(res => {
this.city = res.data.city;
})
//为了不显示undefined,添加了定时来缓解加载的时间
if (timer != null) {
clearTimeout(timer);
}
timer = setTimeout(function() {
_self.init();
}, 300);
},
methods: {
init() {
this.$u.api.getDetailById({
id: this.id
}).then(res => {
this.news = res.data;
this.isShow = true;
})
this.$u.api.getCommentTree({
nid: this.id
}).then(res => {
this.commentList = res.data;
this.changeCreateTime(this.commentList);
this.scope();
})
this.$u.api.getNewsUserStatus({
nid: this.id
}).then(res => {
this.status = res.data;
})
},
// 改变评论时间
changeCreateTime(e) {
e.map(item => {
item.createTime = time.timeago(item.createTime);
if (item.commentChildren.length !== 0) {
this.changeCreateTime(item.commentChildren)
}
return item;
})
},
// 写评论
writeComment() {
if (this.city === null) {
uni.showModal({
title: '提示',
content: '你还未授权登录,请先授权',
success: function (res) {
if (res.confirm) {
uni.switchTab({
url:'../../pages/me/me'
})
} else if (res.cancel) {
console.log('用户点击取消');
}
}
});
} else {
this.$u.api.writeComment({
nid: this.id,
pid: 0,
content: this.wContent
}).then(res => {
if (res.msg === '评论成功') {
this.wContent = '';
this.$refs.uToast.show({
title: '评论成功',
type: 'success'
})
this.init()
} else {
this.$refs.uToast.show({
title: '评论出错,请登录',
type: 'warning'
})
}
})
}
},
// 回复评论
reply(nickname, cid, uid) {
this.nickname = nickname;
this.cid = cid;
if (uid === this.uid) {
this.mySheetShow = true;
} else {
this.otherSheetShow = true;
}
},
// 点击用户评论弹出遮罩
maskClick() {
this.isInput = false;
this.focus = false;
this.wContent = '';
},
// 点击操作菜单
mySheetClick(index) {
if (index === 0) {
this.isInput = 1;
this.focus = 1;
} else if (index === 1) {
this.$u.api.deleteNewsCommentById({
id: this.cid
}).then(res => {
if (res.msg === '删除成功') {
this.$refs.uToast.show({
title: '删除成功',
type: 'success'
})
this.init();
}
})
}
},
otherSheetClick(index) {
if (index === 0) {
this.isInput = 1;
this.focus = 1;
} else if (index === 1) {
uni.navigateTo({
url: `../../mePac/complain/complain?id=${this.cid}&module=资讯评论`
})
}
},
// 回复父级评论
writePComment() {
this.$u.api.writeComment({
nid: this.id,
pid: this.cid,
content: this.wContent
}).then(res => {
if (res.msg === '评论成功') {
this.wContent = '';
this.isInput = false;
this.$refs.uToast.show({
title: '评论成功',
type: 'success'
})
this.init();
} else {
this.$refs.uToast.show({
title: '评论出错,请登录',
type: 'warning'
})
}
})
},
// 展开回复数按钮
changeStatus(index) {
if (this.scopesDefault[index] == true) {
this.$set(this.scopesDefault, index, false)
} else {
this.$set(this.scopesDefault, index, this.scopes[index])
}
},
scope() {
this.commentList.forEach((item, index) => {
this.scopesDefault[index] = false
if ('commentChildren' in item) {
this.scopes[index] = true
} else {
this.scopes[index] = false
}
})
},
updatePage() {
this.init();
},
}
}
</script>
4.2 Java构建所有评论树状结构
(1)pojo层
@Data
public class CommentDetail extends FNewsComment {
//昵称
private String nickname;
//头像
private String avatar;
//资讯图片
private String img;
//资讯标题
private String title;
// 子评论
private ArrayList<CommentDetail> commentChildren;
}
@Data
@ToString
@EqualsAndHashCode
@NoArgsConstructor
@Accessors(chain = true)
@TableName("f_news_comment")
public class FNewsComment implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private String id;
/**
* 评论人id
*/
@Excel(name = "评论人id")
private String uid;
/**
* 新闻id
*/
@Excel(name = "新闻id")
private String nid;
/**
* 父ID, 0表示一级分类
*/
@Excel(name = "父ID, 0表示一级分类")
private String pid;
/**
* 点赞数
*/
@Excel(name = "点赞数")
private Integer likes;
/**
* 回复数
*/
@Excel(name = "回复数")
private Integer replies;
/**
* 评论
*/
@Excel(name = "评论")
private String content;
/**
* 是否已读,在用户的信箱显示
*/
@Excel(name = "是否已读,在用户的信箱显示")
private Boolean isread;
/**
* isclick
*/
@Excel(name = "isclick")
private Boolean isclick;
/**
* 是否审核
*/
@Excel(name = "是否审核")
private Boolean checked;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 修改时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
(2)service层
public interface IFNewsCommentService extends IService<FNewsComment> {
ArrayList<CommentDetail> getPCommentsByNid(String nid);
ArrayList<CommentDetail> selectAllNotBase();
(3)mapper文件
<select id="getPCommentsByNid" resultType="com.ruoyi.fitness.dto.CommentDetail">
select c.id,
c.pid,
c.nid,
c.uid,
c.likes,
c.content,
c.create_time,
c.replies,
u.nickname,
u.avatar
from f_news_comment c
left join f_user u on c.uid = u.id
where c.nid = #{nid}
and c.pid = 0
and c.checked = 1
order by c.id desc
</select>
<select id="selectAllNotBase" resultType="com.ruoyi.fitness.dto.CommentDetail">
select c.id,
c.pid,
c.nid,
c.uid,
c.likes,
c.content,
c.create_time,
c.replies,
u.nickname,
u.avatar
from f_news_comment c
left join f_user u on c.uid = u.id
where c.pid != 0
and c.checked = 1
</select>
(4)controller层
@ApiOperation("获取不同新闻的所有评论的树状结构")
@GetMapping("getCommentTree/{nid}")
@CachePut(value = "getCommentTree", key = "'nid_'+#nid", cacheManager = "redisExpire")
public R getCommentTree(@PathVariable String nid) {
ArrayList<CommentDetail> commentBase = ifNewsCommentService.getPCommentsByNid(nid);
ArrayList<CommentDetail> commentNotBase = ifNewsCommentService.selectAllNotBase();
for (CommentDetail comment : commentBase) {
ArrayList<CommentDetail> comments = iterateComment(commentNotBase, comment.getId());
comment.setCommentChildren(comments);
}
return R.success("获取成功", commentBase);
}
/**
* 多级评论查询方法
*
* @param comments 不包含最高层次评论的评论集合
* @param pid 父类id
* @return
*/
public ArrayList<CommentDetail> iterateComment(ArrayList<CommentDetail> comments, String pid) {
ArrayList<CommentDetail> result = new ArrayList<>();
for (CommentDetail comment : comments) {
String commentId = comment.getId();
String commentPid = comment.getPid();
if (StringUtils.isNotEmpty(commentPid)) {
if (commentPid.equals(pid)) {
// 递归查询当前子评论的子评论
ArrayList<CommentDetail> iterateComment = iterateComment(comments, commentId);
comment.setCommentChildren(iterateComment);
result.add(comment);
}
}
}
return result;
}
5. 演示视频
在uniapp微信小程序中实现无限级评论(vue递归组件+Java构建所有评论树状结构)
6. 总结
做这个功能,说实话搞了很久,它的实现思路主要就是在后台将所有评论构建成树状结构后,在前端使用递归组件进行显示,然而,还是有点不完美的地方,点赞功能我只搞了父级评论的,在子级那里无法点赞(因为我觉得实现起来太复杂,主要是递归的id),所以,你看到我在子级评论那里把点赞图标给删了。如果你们实现了,分享出来吧。
就这样了,不懂留言吧 🍔🍔🍔