多级留言/评论的功能实现——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…

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) 一.元组 &#…

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

Linux——进程与线程

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

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

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

智能界面设计:数字孪生与大数据结合的美学典范

智能界面设计&#xff1a;数字孪生与大数据结合的美学典范 引言 在数字化浪潮的推动下&#xff0c;智能界面设计成为了连接用户与技术的重要桥梁。数字孪生技术与大数据的结合&#xff0c;不仅为UI设计带来了前所未有的创新机遇&#xff0c;更成为了美学与功能性融合的典范。…

C#_初识变量类型与方法

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace ConsoleApp2 {class Program{static void Main(string[] args){///--------常用的变量类型float a 3.12f; //单精度32bit浮点型后缀要加fdou…

个人博客网站开发笔记3

文章目录 前言p4 Front Matterp5 配置文件p6 命令p7 部署新的教学视频部署博客到github找视频教程也是一个技能详细步骤安装主题安装渲染器修改主题创建gitub仓库生成密钥验证密钥是否匹配修改config文件推送到github 前言 主要是安装啥的比较费劲 现在已经比较简单了感觉 之…

Android 共享内存

Parcelable 和 Serializable 区别 Serializable IO完成&#xff08;通过磁盘文件读写&#xff09; Parcelable C 对象指针 来实现共享内存 import android.os.Parcel; import androidx.annotation.NonNull;public class ApiResponseBean extends Throwable implements Parce…

基于 Java 的浏览器——JxBrowser使用分享

软件介绍 JxBrowser 是一个基于 Java 的浏览器&#xff0c;它使用 Chromium 引擎来提供高性能的网页渲染和丰富的功能。它支持多种 GUI 框架&#xff0c;如 Swing、JavaFX 和 SWT&#xff0c;使得在 Java 应用程序中嵌入浏览器组件变得简单。 JxBrowser 是一个适用于多种用途…

学习笔记——STM32F103的V3版本——3*3矩阵键盘控制数码管

一.硬件 1.数码管 2.3*3的矩阵键盘&#xff08;自己做的模块&#xff08;手残党一枚&#xff09;&#xff09; 3.总体连接 二.在Keil5中的部分软代码 test.c中&#xff1a; #include "sys.h" #include "usart.h" #include "delay.h" #include …

“高考钉子户”唐尚珺决定再战2024年高考

“高考钉子户”唐尚珺决定在2024年再次参加高考&#xff0c;这个选择确实很特别也很有趣。十几年连续参加高考&#xff0c;他已经积累了大量的备考经验和应试技巧。这样的经验对于高考辅导机构来说无疑是非常宝贵的资源&#xff0c;他如果选择去辅导机构当老师&#xff0c;应该…

Vitis HLS 学习笔记--控制驱动任务示例

目录 1. 简介 2. 代码解析 2.1 kernel 代码回顾 2.2 功能分析 2.3 查看综合报告 2.4 查看 Schedule Viewer 2.5 查看 Dataflow Viewer 3. Vitis IDE的关键设置 3.1 加载数据文件 3.2 设置 Flow Target 3.3 配置 fifo 深度 4. 总结 1. 简介 本文对《Vitis HLS 学习…