评论盖楼 Vue使用组件递归,组件递归传值,实现评论盖楼功能

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的上层评论,我们需要组件的递归
方便学习我们先创建出最外层的评论

  1. 创建3个组件,第一个是父组件,显示所有评论
  2. 第二个是评论子组件,是作为第2层或者之后的评论块,要组件里面再调用组件(递归组件)
  3. 第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>

递归组件传值,模拟流程
在这里插入图片描述

有很多的记录都写在代码块的注释上面
求大牛们轻喷,只是作为记录学习,可以的话可以指出错误地方,多多指点,不胜感激涕零!

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值