Vue使用组件递归,组件递归传值,实现评论盖楼功能
这里面有使用 vant框架,moment.js ,axios等其他框架,只是自己练习,模仿业务
实现效果:
- 实现评论的显示
- 服务器给出的数据为
{data: {…}, status: 200, statusText: "OK", headers: {…}, config: {…}, …}
config: {url: "/post_comment/7", headers: {…}, baseURL: "http://127.0.0.1:3000", transformRequest: Array(1), transformResponse: Array(1), …}
data:
data: Array(3)
0:
content: "初级评论,只需要获取文本域的value值,还有文章id,不需要用户id,要控制文本的输入框,显示隐藏问题"
create_date: "2020-10-28T14:57:29.000Z"
id: 61
parent: Object
content: "二级评论回复需要获取评论块的回复点击事件,还有子组件参数的值,父组件再传一个评论的数据给底部组件,让底部组件发送请求,在又显示到页面上"
create_date: "2020-10-28T14:54:45.000Z"
id: 60
parent: Object
content: "一级评论:父组件传值给底部评论子组件,把遍历评论列表的值传传给底部子组件,子组件再发请求,成功后又传事件触发父组件从新发请求,刷新页面"
create_date: "2020-10-28T14:49:24.000Z"
id: 59
parent: null
给出的数据都有一个parent的上层评论,我们需要组件的递归
方便学习我们先创建出最外层的评论
- 创建3个组件,第一个是父组件,显示所有评论
- 第二个是评论子组件,是作为第2层或者之后的评论块,要组件里面再调用组件(递归组件)
- 第3个底部评论块组件,作用是发送评论功能,作为点击显示输入框(文本域),显示评论条数,收藏显示
我建议先看评论列表父组件的代码,再看评论块代码,最后看底部功能代码
commentList.vue创建我们评论列表结构
父组件结构
<template>
<div class="comment">
<mytopfun title="精彩跟帖">
<van-icon name="arrow-left" slot="left" @click="$router.go(-1)" />
<van-icon name="home-o" slot="right" style="visibility: hidden" />
</mytopfun>
<div class="lists">
<!-- 第一层 commentList为获取评论的数据 遍历渲染出来 -->
<div class="item" v-for="(item, index) in commentList" :key="index">
<div class="head" style="margin-bottom: 10px">
<img :src="item.user.head_img" alt />
<div>
<!--渲染出发布者名字-->
<p>{{ item.user.nickname }}</p>
<!-- 这里是发布的相对时间,我们使用了一个全局过滤器,使用moment.js -->
<span>{{ item.create_date | getMoment }}</span>
</div>
<!-- 第一层的单击回复,把这一条的评论数据传给底部功能子组件 底部子监听
数据,确定是是否文本域弹出 -->
<span @click="backComment(item)">回复</span>
</div>
<!-- 第二层,这个是评论块子组件 ,commentList下面的parent相同
因为可能值只用最外层评论,所以我们加一个v-if判断如果 获取的commentList数据
里面parent为null,就不渲染,不然报错-->
<!-- :parent="item.parent" 父传子给出评论数据下面的 parent对象数据
给子组件进行渲染-->
<!-- @getComment="getComment" 点击最回复评论 触发事件,子传父,
传下parent数据给父组件,再又父组件传给底部功能子组件 -->
<commentBlock
:parent="item.parent"
v-if="item.parent"
@repalycomment="getComment"
></commentBlock>
<div class="text">{{ item.content }}</div>
</div>
</div>
<!-- 底部功能子组件加上组件 传文章数据,渲染底部评论数量 commentObj传评论的用户id-->
<myfooted
:articaldetail="articaldetail"
@refresh="refresh"
:commentObj="commentObj"
@changeObj="changeObj"
></myfooted>
<!-- articaldetail 为当前文章的详情 因为子组件需要显示文章的评论条数,还有是否关注
所以必须要传一个文章详情-->
<!-- @refresh="refresh" 这个是底部子组件,传给父组件的事件,当子组件完成评论的发送
父组件需要重新请求获取评论数据,动态渲染页面 -->
</div>
</template>
commentList.vue 父组件页面js代码显示
<script>
// 引入 moment过滤器
import { getMoment } from "@/filter/myfilter";
// 引头部
import mytopfun from "@/components/mytopfun";
// 引入请求api
import { post_comment } from "@/apis/article";
// 引入axios 获取 基准路径
import axios from "@/utils/myaxios";
import commentBlock from "@/components/commentBlock";
// 引入底部组件
import myfooted from "@/components/myfooted";
// 调用文章api
import { getAritcalDetail } from "@/apis/article.js";
export default {
components: {
mytopfun,
commentBlock,
myfooted,
},
filters: {
//注册过滤器
getMoment,
},
data() {
return {
// 数据存放地方
commentList: [], //存评论列表数据
articaldetail: {}, //存文章数据
commentObj: {}, //遍历得出的单条评论数据,这个数据要保存在data里面,
//在发送给底部功能子组件
};
},
methods: {
// 二级评论块,子传父 v是parent对象数据,就是子组件的评论数据 ,
getComment(v) {
console.log(v);
this.commentObj = v;
},
//把传给底部评论 当文本框隐藏isFoucs=false时候值转为null 也是子传父,父改传给子的参数
changeObj() {
this.commentObj = null;
},
// 点击回复的时候把值传个底部文本域组件 我们需要把点击回复的那一条评论数据
// 点击回复,给子组件传用户id 发送给底部组件
backComment(item) {
console.log(item);
// 把item 传给底部组件
this.commentObj = item;
},
//底部功能子组件完成发送,再一个子传父,发送一个事件,让父组件触发
//重新渲染页面,把页面返回到顶部
refresh() {
this.init();
// 返回到顶部,简单方式
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
},
// 封装评论列表函数让评论完之后刷新
async init() {
//这个是获取评论列表的axios请求
let res = await post_comment(this.$route.params.id);
console.log(res);
//改找数据,加基准路径,把没有头像的用户添加默认头像
this.commentList = res.data.data.map((item) => {
if (item.user.head_img) {
item.user.head_img = item.user.head_img;
} else {
item.user.head_img =
axios.defaults.baseURL + "/uploads/image/default.jpeg";
}
return item;
});
// 获取文章信息 为什么还有调用:因为实时刷新,评论长度
let result = await getAritcalDetail(this.$route.params.id);
// console.log(result.data.data);
this.articaldetail = result.data.data;
},
},
//页面开始渲染前,我们就必须请求数据,评论列表数据,还有文章数据
async mounted() {
//先获取 评论列表
this.init();
// let res = await post_comment(this.$route.params.id);
// // console.log(res.data.data);
// // 有一些没有图片要动态渲染
// // console.log(res.data.data[0]create_date);
// this.commentList = res.data.data.map((item) => {
// if (item.user.head_img) {
// item.user.head_img = item.user.head_img;
// } else {
// item.user.head_img =
// axios.defaults.baseURL + "/uploads/image/default.jpeg";
// }
// return item;
// });
// 获取到数据,渲染第一层,注意commentList 里面有一个parent的对象,下面开始是第2层
// console.log(this.commentList);
// 使用底部组件,我们必选也要获取传给底部组件的参数,也就是文章详情的参数
// let result = await getAritcalDetail(this.$route.params.id);
// console.log(result.data.data);
// this.articaldetail = result.data.data;
},
};
</script>
css的代码 (不重点,可以略过)
<style lang="less" scoped>
.lists {
border-top: 5px solid #ddd;
padding: 0 15px;
.item {
padding: 10px 0;
border-bottom: 1px solid #ccc;
.head {
display: flex;
justify-content: space-between;
align-items: center;
> img {
width: 50/360 * 100vw;
height: 50/360 * 100vw;
display: block;
border-radius: 50%;
}
> div {
flex: 1;
display: flex;
flex-direction: column;
margin-left: 10px;
> span {
font-size: 12px;
color: #999;
line-height: 25px;
}
}
> span {
color: #999;
font-size: 13px;
}
}
.text {
font-size: 14px;
color: #333;
padding: 20px 0 10px 0;
}
}
}
</style>
myfooted.vue底部功能组件的代码
<template>
<div class="comment">
<div class="addcomment" v-show="!isFocus">
<input type="text" placeholder="写跟帖" @focus="handlerFocus" />
<!-- 点击跳转到评论详情页 -->
<span
class="comment"
@click="$router.push({ path: '/commentList/' + articaldetail.id })"
>
<i class="iconfont iconpinglun-"></i>
<!-- 渲染出评论数据 articaldetail为评论列表父组件出过来的值过来 -->
<em>{{ articaldetail.comment_length }}</em>
</span>
<i
class="iconfont iconshoucang"
@click="handlerstar"
:class="{ star: articaldetail.has_star }"
></i>
<i class="iconfont iconfenxiang"></i>
</div>
<div class="inputcomment" v-show="isFocus">
<textarea ref="commtext" rows="5" ></textarea>
<div>
<!-- 这个重点是 点击发送axios请求要获取文本域的value 再获取文章的id,看下是否有
点击回复,而传入回复的用户id信息-->
<span @click="sendComment">发 送</span>
<!-- 按取消 把文本域隐藏 isFocus=false -->
<span @click="cancelComment">取 消</span>
</div>
</div>
</div>
</template>
子组件底部功能模块就比较简单,就父传子(请求数据),子传父(发送完成,发送一个事件给父元素,让父元素重新获取评论数据渲染页面),子作为发送axios请求 就是所有的参数都要传到这个组件来
上js代码
<script>
// 引用封装发表评论api
import { post_star, send_comment } from "@/apis/article";
export default {
//父组件传入的参数:articaldetail文章信息
//commentObj
props: {
//文章信息为必填属性
articaldetail: {
requires: true,
},
//默认参数为null
commentObj: {
default: null,
},
},
data() {
return {
isFocus: false,
};
},
watch: {
commentObj() {
// 监听,有值就弹框
if (this.commentObj) {
this.isFocus = true;
//显示就有关注
setTimeout(() => {
this.$refs.commtext.focus();
}, 10);
}
},
//这个监听有点考虑得是如果从false到true的时候也会触发呀
//下面我就是吧改变commentObj的子传父到点击取消
// if (!this.isFocus) {
// this.$emit("changeObj");
// }
// },
},
methods: {
// 按取消文本域隐藏
cancelComment() {
this.isFocus = false;
// 重置obj为null,只能让父组件进行obj的重置
//在这里可以吧子传父的事件加到这里
//这个是用户点开又不评论,点击取消,隐藏了文本域,commentObj值也没有改变监听不会触发
//所以我们要手动吧commentObj改为null,但是子组件不能修改父组件传递的参数,
//只能发一个子传父的事件,让父组件改参数
this.$emit("chageObj");
},
//发送评论按钮
async sendComment() {
// console.log(this.articaldetail.id);
//params(为参数)
let params = {
content: this.$refs.commtext.value,
};
// 当有数据传入,获取用户id,加入进参数里面
if (this.commentObj) {
params.parent_id = this.commentObj.id;
}
// console.log(params);
let res = await send_comment(this.articaldetail.id, params);
// console.log(res);
if (res.data.message == "评论发布成功") {
// 提示信息
this.$toast.success(res.data.message);
// 清空文本域
this.$refs.commtext.value = "";
this.isFocus = false;
// 发一个事件,让评论列表刷新,就是子传父
this.$emit("refresh");
} else {
this.$toast.fail(res.data.message);
}
},
//这个是我想点击功能模块,就能把文本域显示并且自动获取焦点,但是并没有实现,大家可以在评论
// 加一个setTimeout就可以解决文本域的获取焦点
//下面告诉下我
handlerFocus() {
this.isFocus = !this.isFocus;
// this.$refs.commtext.focus();
setTimeout(() => {
this.$refs.commtext.focus();
}, 10);
},
//功能之一,点击文章收藏,需要改变字体图标颜色,还要发送请求,给用户提示
// 不需要在父组件处理,只要在子组件处理
async handlerstar() {
let res = await post_star(this.articaldetail.id);
// console.log(res);
this.articaldetail.has_star = !this.articaldetail.has_star;
this.$toast.success(res.data.message);
},
},
};
</script>
底部子组件 css代码(可以直接跳过不重要)
<style lang="less" scoped>
.inputcomment {
position: fixed;
bottom: 0;
left: 0;
padding: 10px;
box-sizing: border-box;
width: 100%;
display: flex;
background-color: #fff;
align-items: flex-end;
textarea {
flex: 3;
background-color: #eee;
border: none;
border-radius: 10px;
padding: 10px;
}
div {
padding: 20px;
}
span {
display: block;
flex: 1;
height: 24px;
line-height: 24px;
padding: 0 10px;
background-color: #f00;
color: #fff;
text-align: center;
border-radius: 6px;
font-size: 13px;
margin: 10px 0;
}
}
.addcomment {
width: 100%;
box-sizing: border-box;
padding: 10px;
margin-top: 20px;
display: flex;
text-align: center;
position: fixed;
bottom: 0;
left: 0;
background-color: #fff;
> input {
flex: 4;
height: 30px;
line-height: 30px;
border-radius: 15px;
border: none;
background-color: #eee;
padding-left: 20px;
font-size: 14px;
}
i {
font-size: 20px;
}
.star {
color: #ffd102;
}
> span {
flex: 1;
position: relative;
> em {
position: absolute;
right: 0;
top: -5px;
font-size: 10px;
background-color: #f00;
color: #fff;
border-radius: 5px;
padding: 3px 5px;
}
}
> i {
flex: 1;
}
}
</style>
commentBlock.vue评论块子组件(可以的话先看这个组件,不要先看底部发表评论组件)
<template>
<div class="commentBlock">
<div class="heaed">
<div class="left">
<p>{{ parent.user.nickname }}</p>
<span>{{ parent.create_date | getMoment }}</span>
</div>
<!-- 点击子组件回复触发父组件 需要传入当前的评论数对象参数 -->
<!-- 定义一个点击的子传父把评论的参数发给父组件,
再由父组件传给底部发送评论的子组件 -->
<div class="right" @click="clickreplay(parent)">回复</div>
</div>
<!-- 在这里我们复用组件 因为我们不知道下面还有多少个parent
出口是v-if 没有 parent.parent -->
<!-- commentItem 本组件,使用name 命名,作用递归-->
<!-- :parent="parent.parent" 重点把数据的下层评论传给下一个递归组件 -->
<!-- 添加一个递归组件传值 @repalyComment="repalyComment"事件
递归组件要每一次都要把点击回复的那一层数据(parent)的值传给上面一层,再传给上面一层 -->
<commentItem :parent="parent.parent"
v-if="parent.parent"
@repalyComment="sendComment">
</commentItem>
<div class="text">{{ parent.content }}</div>
</div>
</template>
js代码
<script>
// 引入 moment过滤器
import { getMoment } from "@/filter/myfilter";
// import moment from "moment";
export default {
// 传入parent值
props: ["parent"], //这里接收评论参数,就算是下一个递归组件,我们也要传参数
//组件递归的写法,声明一个name 可以在子组件中使用自己()
name: "commentItem",
filters: {
getMoment,
},
methods: {
// 点击回复 把当前用户id 传给过去给父组件
clickreplay(parent) {
this.$emit("repalycomment", parent);
},
//这里的v==parent,传的就是点击那回复那一个子组件的评论数据
//组件递归里面传值
sendComment(v){
this.$emit('replaycomment',v)
//注意这里我们发送事件要和接受事件的名字一样,就是@replaycomment 要和this.$emit('replaycomment') 发送值一样
}
},
};
</script>
css代码
<style lang="less" scoped>
.commentBlock {
box-sizing: border-box;
padding: 10px;
width: 100%;
// height: 100px;
border: 1px solid #ccc;
.heaed {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.left {
display: flex;
justify-content: center;
align-items: center;
> p {
font-size: 16px;
font-weight: 400;
}
> span {
margin-left: 5px;
font-size: 13px;
color: #999;
}
}
.right {
font-size: 13px;
color: #999;
}
.text {
margin-top: 20px;
font-size: 14px;
color: #333;
}
}
</style>
递归组件传值,模拟流程
有很多的记录都写在代码块的注释上面
求大牛们轻喷,只是作为记录学习,可以的话可以指出错误地方,多多指点,不胜感激涕零!