多级留言/评论的功能实现——Vue3前端篇

文章目录

  • 思路分析
  • 封装组件
    • 父组件
      • 模板
      • 逻辑
      • 样式
    • 子组件——二级留言
      • 模板
      • 逻辑
      • 样式
    • 子组件——三级留言以上
      • 模板
      • 逻辑
      • 样式
  • 留言组件的使用

写完论文了,来把评论的前端部分补一下。
前端的实现思路是自己摸索出来的,没找到可以符合自己需求的参考,有问题或者有优化的建议欢迎指正。

思路分析

当时写完后端的时候就在思考应该怎么处理响应数据才能做到多级评论的展示,感觉应该 不复杂,但是经验有限,写起来有些吃力。

本来我只用了一个组件,然后在里面实现多级的嵌套,但是发现这样使得单个文件比较臃肿,而且逻辑比较复杂。

于是我拆分了一下,两个组件,一个父组件、一个子组件。父组件只展示顶级留言,子组件展示所有子留言,但是这没有解决二级与三级以上留言的区分。

最终的版本,一个父组件,两个子组件。父组件同样只展示顶级留言,但是父组件中引入了两个子组件,分别展示了二级留言和三级以上留言,三级以上留言使用了递归来实现。

实现如图
在这里插入图片描述

封装组件

每一个组件都是单文件组件,模板、逻辑、样式都在同一个文件,但是这里为了让大家更清楚的浏览,将三个部分拆开展示。

父组件

模板

为了解读代码方便,将解释贴在了每段代码前方,建议按照从上至下的顺序去理解代码,根据这个顺序去看对应的逻辑实现。

难点:

  1. 父子组件传值
  2. 回复框的定位:控制回复框仅在当前点击的留言下方出现
<template><div class="comments"><el-card><template #header><div class="comments-header"><h3>留言区</h3></div></template><!-- 编辑区1. 左侧:显示当前登录用户头像2. 中间:输入框,使用 v-model 收集用户输入的内容 comment3. 右侧:在 handlePublish 方法中请求新增留言接口--><div class="editbox"><div class="editbox-left"><el-avatar :size="45" :src="userInfo.avatar" /></div><div class="editbox-middle"><el-inputplaceholder="与其赞同别人的话语,不如自己畅所欲言..."v-model="comment"></el-input></div><div class="editbox-right"><el-button @click="handlePublish(comment)">发布</el-button></div></div><!-- 列表区1. 遍历分页获取的留言列表,并设置唯一的 key 值2. 顶级留言包括:头像、昵称、角色名标签、留言内容、发布时间3. 点击 “回复” 会触发事件 handleReply,同时传入两个参数:当前被回复留言的根ID、当前被回复留言的直接父级IDa. 这两个参数用于给【即将发布的留言】设置根ID与直接父级ID,做到 “回复框的定位” b. 对于二级留言,这两个值传入顶级留言本身的ID就行--><div class="listbox" v-for="(item, index) in commentsList" :key="index"><!-- 顶级留言:这没什么好说的,就直接展示遍历的结果 --><div class="top-level"><div class="listbox-top-user"><el-avatar :size="45" :src="item.userImg" /><p><span>{{ item.createdBy }}</span><span>{{ item.roleName }}</span></p></div><div class="listbox-middle-root">{{ item.comment }}</div><div class="listbox-bottom"><span>发布时间:{{ item.createdAt }}</span><span @click="handleReply(item.id, item.id)">回复</span></div></div><!-- 子留言区1. 这里没有使用在二级组件中引入三级以上组件的方式,因为当时开发的时候感觉传值有点麻烦2. 使用两个子组件同级的形式子留言:二级1. 判断顶层留言是否存在二级子留言,是则引入 SecondComment 子组件2. 父组件传递参数:二级评论 item.children3. 处理 “回复” 功能,使用同一个方法实现(handleReply),这里的根ID和直接父ID是子组件传过来的4. handle-reply 是子组件中声明需要抛出的事件,@handle-reply 代表监听子组件的自定义事件--><div v-if="item.children && item.children.length"><SecondComment:secondComments="item.children"@handle-reply="handleReply"style="margin-left: 0"/><!-- 子留言:三级1. 因为使用的同级结构,所以需要先遍历每一个二级留言,判断其下是否存在子留言,是则引入 ChildComment 子组件。2. 必须要遍历二级留言并设置唯一的 key !!无法直接获取 item.children.children !!!(原因有点忘了...)3. 同样要给子组件传递参数:三级评论 child.children4. 同时将二级留言的发表人昵称传递给子组件,用于非二级留言的子留言显示 “ @nickname ” 5. 同样拥有 “回复” 功能,使用同一个方法实现(handleReply),这里的根ID和直接父ID是子组件传过来的6. 子组件中声明的抛出事件(to-reply)不能与其他组件重复--><templatev-for="(child, childIndex) in item.children":key="childIndex"><template v-if="child.children && child.children.length"><ChildComment:childComments="child.children":parentName="child.createdBy"@to-reply="handleReply"style="margin-left: 65px"/></template></template></div><!-- 回复框1. 使用一个变量 showReply 来控制显示隐藏2. 同时使用变量 showReplyIndex 用来确定是在哪条留言下显示回复框,否则点击 “回复” 会在所有留言下都出现回复框3. 当 handleReply 方法被触发时,改变 showReply 和 showReplyIndex 的值4. 使用 replyComment 收集回复框输入的内容,当触发 handlePublish 方法时作为参数传进去--><divclass="reply-box-container"v-show="showReplyIndex === item.id && showReply"><div class="replybox" id="reply-box"><div class="replybox-left"><el-avatar :size="30" :src="userInfo.avatar" /></div><div class="replybox-middle"><el-input placeholder="回复" v-model="replyComment"></el-input></div><div class="replybox-right"><el-button @click="handlePublish(replyComment)">提交</el-button></div></div></div></div><!-- 分页器:这也是一个单独的组件,此处不做深究,有机会会再出一篇封装分页组件的文章 --><PageQuery:total="total":pageNum="getCommentForm.pageNum":pageSize="getCommentForm.pageSize"@page-size="handlePageSize"@page-num="handlePageNum"/></el-card></div>
</template>

代码对应结构参考:
在这里插入图片描述

逻辑

<script setup>
import { ref, onMounted, reactive } from "vue";
import { getCommentListApi, postAddCommentApi } from "@/api/common";
import PageQuery from "@/components/common/PageQuery.vue";
import ChildComment from "@/components/front/ChildComment.vue";
import { useUserStore } from "@/stores/useUserStore";
import SecondComment from "@/components/front/SecondComment.vue";const { userInfo } = useUserStore();
// 收集 “编辑区” 的输入内容
const comment = ref("");
// 收集 “回复框” 的输入内容
const replyComment = ref("");
// 存储请求回来的数据总数
const total = ref(0);
// 存储请求回来的留言列表
const commentsList = ref();
// 控制回复框的索引
const showReplyIndex = ref(0);
// 控制回复框的显示隐藏
const showReply = ref(false);onMounted(() => {getCommentList();
});// 接收父组件传过来的值——在别的文件中使用留言组件,则改文件为留言组件的父组件
const props = defineProps({
// 关联主体IDmomentId: {type: Number,required: true,},postAddCommentForm: {type: Object,required: true,},
});// 请求当前主体的评论列表
const getCommentForm = reactive({pageNum: 1,pageSize: 10,// ChildPageNum: 1,// ChildPageSize: 2,momentId: props.momentId,
});/*** 获取留言列表*/
const getCommentList = async () => {try {// 封装参数const res = await getCommentListApi(getCommentForm);total.value = res.data.total;commentsList.value = res.data.items;} catch (error) {}
};/*** 显示 回复编辑框*/
const handleReply = (rootCommentId, parentId) => {// 解决只在 当前点击项下 显示回复框showReplyIndex.value = rootCommentId;// 控制显示隐藏showReply.value = !showReply.value;const replyBox = document.querySelector(".reply-box-container");// 更新回复编辑框的属性,作为参数传给父组件// 这里使用到一个知识点:自定义属性// 因为需要实现绑定某回复框并使其含有rootCommentId和parentId,发送新增子留言请求时需要这两个参数replyBox.setAttribute("data-parent-comment-id", parentId);replyBox.setAttribute("data-root-comment-id", rootCommentId);
};/*** 发布/回复 评论*/
const handlePublish = async (comment) => {// 封装请求体:数据从父组件来const params = {comment: comment,momentId: props.postAddCommentForm.momentId,commentType: props.postAddCommentForm.commentType,rootCommentId: null,parentId: null,replyComment: "",};// 子评论 添加属性const replyBox = document.querySelector(".reply-box-container");if (replyBox) {// 获取根评论IDconst rootCommentId = replyBox.getAttribute("data-root-comment-id");// 获取直接父评论IDconst parentId = replyBox.getAttribute("data-parent-comment-id");params.rootCommentId = rootCommentId;params.parentId = parentId;}// 发送请求try {const res = await postAddCommentApi(params);ElMessage.success(res.msg);getCommentList();// 【问题】发布评论后,输入框中的值没有消失} catch (error) {console.log("🚀 ~ handlePublish ~ error:", error);}
};/*** 分页器--当前页的数据量*/
const handlePageSize = (pageSizeVal) => {getCommentForm.pageSize = pageSizeVal.pageSize;getCommentList();
};/*** 分页器--切换页码*/
const handlePageNum = (pageNumVal) => {getCommentForm.pageNum = pageNumVal.pageNum;getCommentList();
};
</script>

样式

<style lang="scss" scoped>
@import "@/assets/css/var.scss";// 留言区
.comments {margin-top: 30px;margin-bottom: 100px;.el-card {width: 80%;margin: 20px auto;}.editbox,.listbox {margin: 0px 20px 20px 20px;display: flex;}// 编辑区.editbox {justify-content: space-between;align-items: center;.editbox-middle {width: 85%;}}// 列表展示区.listbox {flex-direction: column;border-bottom: 1px solid rgb(189, 187, 187);// 时间 + 回复.listbox-bottom {font-size: 12px;color: #9499a0;margin: 10px 0 10px 65px;display: flex;span {display: block;margin-right: 20px;}// 这里 color 换成普通颜色表示即可span:last-child:hover {cursor: pointer;color: $title-color;}}// 信息条.listbox-top-user {display: flex;// 个人信息p {margin-left: 20px;width: 100%;position: relative;span:first-child {color: $second-text;}// 身份标签span:last-child {margin-left: 5px;font-size: 8px;padding: 2px;background-color: $title-color;color: white;border-radius: 5px;position: absolute;}}}// 顶级评论.top-level {// 根评论内容.listbox-middle-root {margin-left: 65px;}}// 回复评论输入框.replybox {margin: 10px 0 20px 65px;display: flex;justify-content: space-between;align-items: center;width: 60%;.replybox-middle {width: 75%;}}// 展示更多.view-more {margin-left: 65px;font-size: 12px;color: #9499a0;}.view-more span:hover,.view-less span:hover {cursor: pointer;color: $title-color;}// 展示更少.view-less {font-size: 12px;color: #9499a0;margin-left: 37px;}}
}
</style>

子组件——二级留言

模板

<!-- 二级评论 --><template><div v-if="props.secondComments && props.secondComments.length"><divclass="sub-reply-container"id="child-reply"v-for="(child, childIndex) in props.secondComments":key="childIndex"><div class="listbox-top-user"><el-avatar :size="30" :src="child.userImg" /><p><span>{{ child.createdBy }}</span><span>{{ child.roleName }}</span></p></div><div class="listbox-middle-root">{{ child.comment }}</div><div class="listbox-bottom"><span>发布时间:{{ child.createdAt }}</span><!-- 回复的是二级评论 --><span @click="handleReply(child.parentId, child.id)">回复</span></div></div></div>
</template>

逻辑

<script setup>
// 接收父组件传过来的值:二级留言
const props = defineProps({secondComments: {type: Array,default: [],},
});// 声明需要抛出的事件
const emit = defineEmits(["handle-reply"]);const handleReply = (rootCommentId, parentId) => {// 【注意】这里不以对象形式包裹发送,会导致嵌套;因为父组件中回复一级评论与子级评论共用一个传值方法emit("handle-reply", rootCommentId, parentId);
};
</script>

样式

<style lang="scss" scoped>
@import "../../assets/css/_var.scss";.sub-reply-container {margin: 20px 0 0 65px;.listbox-top-user {display: flex;p {margin-left: 10px;width: 100%;// 姓名条span:first-child {color: $second-text;}// 身份标签span:nth-child(2) {margin-left: 5px;font-size: 8px;padding: 2px;background-color: $title-color;color: white;border-radius: 5px;position: relative;bottom: 4px;}}}.listbox-middle-root,.listbox-bottom {margin-left: 38px;}.listbox-bottom {font-size: 12px;color: #9499a0;margin: 10px 0 10px 35px;display: flex;span {display: block;margin-right: 20px;}span:last-child:hover {cursor: pointer;color: $title-color;}}
}
</style>

子组件——三级留言以上

模板

<!-- 三级及以上评论 -->
<template><div class="sub-reply-container" v-if="childComments && childComments.length"><div class="sub-reply" v-for="(child, index) in childComments" :key="index"><!-- 渲染内容 --><div class="listbox-top-user"><el-avatar :size="30" :src="child.userImg" /><p><span>{{ child.createdBy }}</span><span>{{ child.roleName }}</span>回复<span>@{{ parentName }}</span></p></div><div class="listbox-middle-root">{{ child.comment }}</div><div class="listbox-bottom"><span>发布时间:{{ child.createdAt }}</span><span @click="handleReply(child.rootCommentId, child.id)">回复</span></div><!-- 递归地渲染子评论的子评论:调用自己 --><ChildComment:childComments="child.children":parentName="child.createdBy"@to-reply="handleReply"/></div></div>
</template>

逻辑

<script setup>
// 接收父组件传过来的值
const props = defineProps({childComments: {type: Array,default: [],},parentName: {type: String,reequire: true,}
});
const childComments = props.childComments;
const parentName = props.parentName;// console.log("🚀 ~ parentName:", parentName);
// console.log("🚀 ~ childComments:", childComments);// 声明需要抛出的事件
const emit = defineEmits(["to-reply"]);const handleReply = (rootCommentId, parentId) => {// 【注意】这里不以对象形式包裹发送,会导致嵌套;父组件中回复一级评论与子级评论共用一个传值方法emit("to-reply", rootCommentId, parentId);
};
</script>

样式

<style lang="scss" scoped>
@import "../../assets/css/_var.scss";.listbox-top-user {display: flex;p {margin-left: 10px;width: 100%;// 姓名条span:first-child {color: $second-text;}// 身份标签span:nth-child(2) {margin-left: 5px;font-size: 8px;padding: 2px;background-color: $title-color;color: white;border-radius: 5px;position: relative;bottom: 4px;}// 父级姓名条span:last-child {color: #0c9dd2;}}
}.listbox-middle-root,
.listbox-bottom {margin-left: 38px;
}.listbox-bottom {font-size: 12px;color: #9499a0;margin: 10px 0 10px 35px;display: flex;span {display: block;margin-right: 20px;}span:last-child:hover {cursor: pointer;color: $title-color;}
}
</style>

两个子组件没有太多额外说明,其实把父组件看懂了,子组件挺简单的,关键地方我都写了注释。有疑问可以在评论区提出,看到会回复的。

留言组件的使用

在药材详情页中使用:

  <!-- 留言区使用时只需要传两个参数:评论类型、当前关联主体ID(就是谁被留言了,就传谁的ID)--><Comment :momentId="getMedicineId" :postAddCommentForm="postAddCommentForm"/>

相关代码

import { useRoute } from "vue-router";
import { reactive } from "vue";
import Comment from "@/components/front/Comment.vue";const route = useRoute();
// 接收通过路由跳转传过来的资讯ID,默认变为字符串
const getMedicineId = Number(route.query.medicineId);// 请求发布评论的请求体
const postAddCommentForm = reactive({comment: "",momentId: Number(getMedicineId),commentType: 1,rootCommentId: null,parentId: null,replyComment: "",
});
// 其实,这里好像可以不用封成一个对象了,单独把 commentType 传过去就行,感兴趣的同学可以试试(记得相应的把留言组件中接收的地方也改一下)

在方剂下进行留言, commentType设置为2;在资讯下进行留言, commentType设置为3;同时传入主体ID即可完成整个留言组件的调用。

以上实现均为个人思考后做出来了,代码还有欠缺,也有优化的地方,大佬路过莫喷。但是欢迎大家提出建议!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/14967.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

windows 7 10 11快捷键到启动页面

1.快速打开用户启动文件夹 shell:startup 方式2&#xff1a;快速打开系统启动文件夹 shell:Common Startup shell:Common Startup

编译器 编译过程 compiling 动态链接库 Linking 接口ABI LTO PGO inline bazel增量编译

编译器 编译过程 compiling 动态链接库 Linking 接口ABI LTO PGO Theory Shared Library Symbol Conflicts (on Linux) 从左往右查找:Note that the linker only looks further down the line when looking for symbols used by but not defined in the current lib.Linux 下…

统计信号处理基础 习题解答10-4

题目&#xff1a; 重复习题10.3&#xff0c;但条件PDF变为&#xff1a; 以及均匀先验。如果非常大&#xff0c;这样先验知识很少&#xff0c;则会出现什么情况。 解答&#xff1a; 如果记 那么&#xff0c;根据条件独立性质&#xff0c;得到&#xff1a; 其中&#xff0c;&am…

linux 生成可执行文件

pip install pyinstaller pyinstaller --onefile xunhuan.py 在centos系统中&#xff0c;安装pyinstaller&#xff0c;然后执行命令&#xff0c;生成文件可以直接调用&#xff0c;比如 /root/dist/xunhuan 在/root/目录下&#xff0c;系统环境一般问题很大&#xff0c;找到…

java单元测试:使用Mockito模拟外部依赖

使用Mock对象来模拟外部依赖是单元测试中的重要技巧&#xff0c;特别是在你需要测试的代码依赖于外部系统&#xff08;如数据库、网络服务等&#xff09;时。Mock对象允许你在不实际调用这些外部系统的情况下测试代码的行为&#xff0c;从而提高测试的独立性和执行速度。 什么…

巧用count与count()

在C#中&#xff0c;talentInnoPfChains.Count() 和 talentInnoPfChains.Count 的性能差异主要取决于 talentInnoPfChains 的类型。这里有两种可能的情况&#xff1a; 如果 talentInnoPfChains 是一个实现了 ICollection<T> 接口的集合&#xff08;如 List<T>, Hash…

NLP与训练模型-GPT-3:探索人工智能语言生成的新纪元

在人工智能领域&#xff0c;自然语言处理&#xff08;NLP&#xff09;一直是备受关注的研究方向之一。随着深度学习技术的发展&#xff0c;尤其是Transformer模型的出现&#xff0c;NLP领域取得了巨大的进步。其中&#xff0c;由OpenAI推出的GPT-3模型更是引起了广泛的关注和热…

SwiftUI中的组合动画(Simultaneous, Sequenced, Exclusive)

了解了常见的几种手势后&#xff0c;接下来我们了解一下组合手势的操作&#xff0c;当一个视图存在多个手势的时候&#xff0c;为了避免手势冲突&#xff0c;SwiftUI提供了自定义手势的方法&#xff0c;比如同时进行&#xff0c;顺序进行等等。 以下是一些常见的多种手势组合使…

关于AI绘画的模型、开源项目、工具、技巧的学习

目录 一、AI绘画的大模型有哪些&#xff1f; 二、Stable Diffusion是一个流行的AI绘画开源项目。 三、AI绘画的开源工具有哪些&#xff1f; 四、AI绘画的技巧 五、最简单的实践 一、AI绘画的大模型有哪些&#xff1f; AI绘画领域中存在多种大模型&#xff0c;每种模型都有…

渗透测试 一个很奇怪的支付漏洞

新手实战刷课网站、好玩又有趣&#xff01; 第一步 打开网站、任意账户名密码登陆发现验证码可重复利用 这时候我们可以试试admin账号、发现如果账号正确会提示账户已存在、反之回显账户密码错误 第二步 既然验证码可以重复利用&#xff1b;而且账号名有回显 这时候我们试…

学习使用博客记录生活

学习使用博客记录生活 新的改变 今天新的开始&#xff0c;让我用图片开始记录吧 看这个背景图片怎么样

人生苦短,我学python之数据类型(上)

个人主页&#xff1a;星纭-CSDN博客 系列文章专栏&#xff1a;Python 踏上取经路&#xff0c;比抵达灵山更重要&#xff01;一起努力一起进步&#xff01; 目录 一.元组 &#xff08;tuple&#xff09; 二.集合&#xff08;set&#xff09; 三.字典(dict) 一.元组 &#…

docker 清空所有镜像日志

Docker清空所有镜像日志流程 1. 查看当前运行的容器 首先&#xff0c;我们需要查看当前正在运行的容器&#xff0c;以确定需要清空日志的容器。 可以使用以下命令查看当前正在运行的容器&#xff1a; docker ps 1. 2. 停止所有运行中的容器 在清空镜像日志之前&#xff0c;我…

MySQL存储过程for循环处理查询结果

在MySQL数据库中&#xff0c;存储过程是一种预编译的SQL语句集&#xff0c;可以被多次调用。在MySQL中使用存储过程查询到结果后&#xff0c;有时候需要对这些结果进行循环处理。 1. 创建表 CREATE TABLE t_job (job_id int(11) unsigned NOT NULL AUTO_INCREMENT,job_name v…

深入了解银行信用卡催收系统

银行信用卡催收系统是一个专门用于管理和执行信用卡逾期账款催收工作的系统。该系统通常具备以下关键功能和特点&#xff1a; 智能呼叫系统&#xff1a;具备自动拨号功能&#xff0c;可以批量拨打逾期客户的电话&#xff0c;播放定制的催收录音信息或直接连接到人工坐席。此外…

崆峒酥饼:端午佳节的美味之选

崆峒酥饼&#xff1a;端午佳节的美味之选 在端午佳节来临之际&#xff0c;崆峒酥饼成为了备受瞩目的佳节之选。崆峒酥饼以其独特的制作工艺和口感&#xff0c;为这个传统节日增添了一份美味与温馨。 崆峒酥饼源自甘肃平凉&#xff0c;是当地的传统名点。它选用优质的面粉、油脂…

Linux——进程与线程

进程与线程 前言一、Linux线程概念线程的优点线程的缺点线程异常线程用途 二、Linux进程VS线程进程和线程 三、Linux线程控制创建线程线程ID及进程地址空间布局线程终止线程等待分离线程 四、习题巩固请简述什么是LWP请简述LWP与pthread_create创建的线程之间的关系简述轻量级进…

Java怎样动态给对象添加属性并赋值【代码实现】

本篇文章主要介绍Java如何给已有实体类动态的添加字段并返回新的实体对象且不影响原来的实体对象结构。 参考代码如下&#xff1a; 引入依赖包 <dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>2.2.2</…

云端升级,智能适配——LDR6282,USB-C接口显示器的最佳选择

华为MateView USB-C接口显示器技术深度解析与科普 随着科技的飞速发展&#xff0c;终端显示产品也迎来了全新的变革。在众多更新迭代中&#xff0c;华为MateView显示器凭借其独特的USB-C接口设计&#xff0c;为用户带来了前所未有的便捷体验。本文将带您深入探索这款显示器的技…