在uniapp微信小程序中实现无限级评论(vue递归组件+Java构建所有评论树状结构)

8 篇文章 0 订阅
3 篇文章 1 订阅

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),所以,你看到我在子级评论那里把点赞图标给删了。如果你们实现了,分享出来吧。

就这样了,不懂留言吧 🍔🍔🍔

Vue3+uni-app实现微信小程序登录流程主要分为以下步骤: 1. 在uni-app项目安装并引入微信小程序登录插件(如wxlogin),在main.js初始化并配置插件参数。 2. 创建一个登录页面,该页面包含点击按钮触发微信登录的操作,可以通过uni.login()方法调用微信小程序登录接口获取code。 3. 接收到微信小程序登录接口返回的code后,将code发送给后端服务器,后端服务器将code和小程序的App ID以及App Secret发送给微信服务器进行登录凭证校验,获取到session_key和openid。 4. 服务器根据openid和session_key生成一个自定义的token,返回给前端。 5. 前端将token保存在本地,使用uni.setStorage()方法进行存储,以便后续的登录状态维持和接口请求验证。 6. 在需要登录验证的页面或组件,通过uni.getStorage()方法获取本地存储的token,并将token添加到请求头,发送给后端服务器进行接口请求。 7. 后端服务器接收到带有token的请求,对token进行校验和解析,验证token是否有效,从而确保用户的登录状态。 总结:通过以上步骤,实现Vue3+uni-app微信小程序的登录流程。用户通过点击按钮触发微信小程序登录接口,后端服务器校验登录凭证,生成token并返回给前端,前端保存token并在请求接口时携带token进行验证,保证了用户的登录状态和接口访问权限的安全性。
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值