【vue组件】简单留言板 可回复 可点赞 无@

使用vue.js实现的留言板组件
效果图如下:
web端
【vue组件】简单留言板 可回复 可点赞 无@_第1张图片
app端
【vue组件】简单留言板 可回复 可点赞 无@_第2张图片
父组件:

<template>
  <div class="msg-all-contain">
    <div class="msg-board-title">留言板</div>
    <div class="msg-board">
      <div class="msg-board-contain">
        <div class="msg-head">
          <img v-if="userAvatar" :src="userAvatar" alt="" />
          <img v-else :src="require('@/assets/default.jpg')" alt="" />
          <textarea
            type="textarea"
            :class="inputStatusClass"
            placeholder="请输入内容..."
            ref="input"
            v-model="newComment"
            cols="60"
            rows="5"
          >
          </textarea>
          <button @click="submit">发表</button>
        </div>
        <div class="msg-content">
          <comments-child
            :comments="comments"
            :count="layerCount"
          ></comments-child>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import CommentsChild from "@/components/comments/CommentsChild";
import dayjs from "dayjs";
export default {
  name: "commentsMessage",
  data() {
    return {
      // 评论列表
      comments: [],
      // 新评论
      newComment: "",
      // 用户头像
      userAvatar: "",
      // 非空判断
      hasNoConent: false,
      // 输入栏状态
      inputStatusClass: "",
      // 计数
      layerCount: 0,
    };
  },
  created() {},
  mounted() {
    this.initData();
  },
  components: {
    "comments-child": CommentsChild,
  },
  methods: {
    // 提交
    submit() {
      var self = this;
      // 提交信息非空验证
      if (!self.newComment || self.newComment === "") {
        self.hasNoConent = true;
        self.inputStatusClass = "no-content-warn";
        return;
      }
      self.inputStatusClass = "";

      // 生成comment对象
      var comment = {
        // 父评论Id
        parentId: "",
        // 评论内容
        text: self.newComment,
        // 发送者id
        senderId: "1",
        // 接收者id
        receiverId: "",
        // 发送时间
        postDate: dayjs().format("YYYY-MM-DD HH:mm"),
        // 发送者头像
        senderAvatar: "",
        // 子评论
        children: [],
        // 发送者姓名
        senderName: "Wendy",
        // comment id
        id: "",
        // 点赞
        likes: 0
      };

      // 新添加的评论插入数组最前面
      self.comments.unshift(comment);
      // 清空评论内容
      self.newComment = "";
    },
    initData() {
      var self = this;
      self.getComments();
    },
    // 获取数据
    getComments() {
      var self = this;
      // 这是自己伪造的数据
      // 要根据接口要求进行修改
      self.comments = [
        {
          children: [
            {
              parentId: "55824b1c",
              text: "回复信息",
              senderId: "25d85a0f",
              receiverId: "25d85a0f",
              postDate: "2022-08-05 12:10",
              senderAvatar: "",
              children: [],
              senderName: "Wendy",
              id: "f0e3a81b",
              likes: 0,
              receiverName: "Irene",
              receiverAvatar: "",
            },
          ],
          id: "55824b1c",
          postDate: "2022-08-05 8:09",
          senderName: "Wendy",
          senderAvatar: "",
          receiverName: null,
          receiverAvatar: null,
          parentId: null,
          text: "测试信息",
          senderId: "25d85a0f",
          receiverId: null,
          likes: 0,
        },
      ];
    },
  },
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
/* 评论头像 */
.msg-head img {
  width: 55px;
  height: 55px;
  border-radius: 50%;
  position: absolute;
  top: 22px;
  left: 13px;
}
.msg-all-contain {
  width: 100%;
  height: 100%;
  overflow-y: auto;
}

.msg-board-contain {
  letter-spacing: 1px;
  padding: 0 10px;
}

/* 信息栏标题 */
.msg-board-title {
  width: auto;
  text-align: center;
  font-size: 28px;
  font-weight: 300;
  margin: 0 0 1.8rem 0;
  min-height: 20px;
  color: #000 !important;
  font-family: "Lato", Verdana, sans-serif !important;
}
.msg-head {
  background-color: rgb(248, 248, 248);
  position: relative;
  height: 130px;
  border-radius: 5px;
}

/* 评论输入 */
.msg-head textarea {
  position: absolute;
  top: 13px;
  left: 85px;
  max-height: 60px;
  border-radius: 5px;
  outline: none;
  width: calc(100% - 300px);
  font-size: 16px;
  padding: 20px;
  border: 2px solid #f8f8f8;
  resize: none;
}
/* 发布评论按钮 */
.msg-head button {
  position: absolute;
  top: 13px;
  right: 35px;
  width: 100px;
  height: 100px;
  border: 0;
  border-radius: 5px;
  font-size: 18px;
  font-weight: 500;
  color: #fff !important;
  background-color: #00a1d6;
  transition: 0.1s;
  cursor: pointer;
  letter-spacing: 2px;
}
/* 鼠标经过字体加粗 */
.msg-head button:hover {
  /*font-weight: 600;*/
}

.msg-content {
  overflow-y: auto;
}

/* 评论内容区域 */
.msg-content .child-comment {
  display: flex;
  position: relative;
  padding: 18px 10px 18px 10px;
}

@media (max-width: 900px) {
  .msg-head img {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    position: absolute;
    top: 22px;
    left: 13px;
  }
  .msg-head textarea {
    position: absolute;
    top: 13px;
    left: 70px;
    height: 55px;
    border-radius: 5px;
    outline: none;
    width: calc(100% - 200px);
    font-size: 15px;
    padding: 10px;
    border: 2px solid #f8f8f8;
    resize: none;
  }
  .msg-head button {
    position: absolute;
    top: 13px;
    right: 16px;
    width: 80px;
    height: 77px;
    border: 0;
    border-radius: 5px;
    font-size: 14px;
    font-weight: 500;
    color: #fff !important;
    background-color: #00a1d6;
    transition: 0.1s;
    cursor: pointer;
    letter-spacing: 2px;
  }
}
</style>


子组件

<template>
  <div>
    <div
      :class="count > 0 ? '' : 'comments-child-contain'"
      v-for="(item, index) in comments"
      :key="index"
    >
      <!--style 根据层级缩进-->
      <div class="comments-child" :style="{ paddingLeft: 30 * count + 'px' }">
        <div
          :class="count > 0 ? 'comments-child-img-sm' : 'comments-child-img'"
        >
          <img v-if="item.senderAvatar" :src="item.senderAvatar" alt="" />
          <img v-else :src="require('@/assets/default.jpg')" alt="" />
        </div>
        <div class="comments-child-content">
          <!-- 用户信息 -->
          <div class="comments-child-username-contain">
            <h3 class="comments-child-username">{{ item.senderName }}</h3>
            <div
              v-if="item.receiverId && item.receiverId !== ''"
              class="comments-child-replay"
            >
              <span class="reply-text">回复</span>
              <h4 class="comments-child-at-username">
                @{{ item.receiverName }}
              </h4>
            </div>
          </div>
          <!-- 评论内容 -->
          <p class="comments-comments-child">
            {{ item.text }}
          </p>
          <div class="comments-child-bottom-contain">
            <!-- 发布时间 -->
            <span class="comments-child-time"> {{ item.postDate }} </span>
            <!--删除和回复-->
            <div class="comments-child-right">
              <span class="fa fa-thumbs-up delete" @click="commentLike(item)">{{
                item.likes
              }}</span>
              <span
                class="fa fa-trash-o delete"
                @click="commentDelete(item, $event)"
                v-show="false"
                >删除</span
              >
              <span
                v-if="layerCount"
                class="fa fa-comment-o comments"
                @click="goReply(item, $event)"
                >回复</span
              >
            </div>
          </div>
          <div class="reply-comment">
            <img v-if="userAvatar" :src="userAvatar" alt="" />
            <img v-else :src="require('@/assets/default.jpg')" alt="" />
            <input
              :class="inputStatusClass"
              type="text"
              v-model="replyComment"
              @keyup.enter="replySumbit(item, $event)"
            />
            <button @click="replySumbit(item, $event)">回复</button>
          </div>
        </div>
      </div>
      <!--递归调用-->
      <div v-if="item.children">
        <comments-child
          :comments="item.children"
          :count="layerCount"
        ></comments-child>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "CommentsChild",
  data() {
    return {
      // 回复评论
      replyComment: "",
      // 非空验证
      hasNoConent: false,
      inputStatusClass: "",
      layerCount: 0,
      userAvatar: "",
      // 点赞数
      like: 0,
    };
  },
  created() {
    var _this = this;
    _this.layerCount = _this.count;
    _this.layerCount++;
    // 子评论限制为一层
    // if (_this.layerCount < 1) {
    //   _this.layerCount++;
    // } else {
    //   _this.layerCount = 1;
    // }
  },

  mounted() {
  },
  props: {
    // 卡片内容
    comments: {
      type: Array,
      required: true,
    },
    // 子评论计数
    count: {
      type: Number,
      default: 0,
    },
  },

  watch: {},
  methods: {
    commentDelete(obj) {},
    // 点赞
    commentLike(obj) {
      var _this = this;
      obj.likes++;
    },
    // 显示回复输入框
    goReply(obj, event) {
      var _this = this;
      _this.inputStatusClass = "";
      _this.replyComment = "";
      var _thisDom = event.currentTarget;
      // 注意 nextElementSibling
      var replyDom = _thisDom.parentNode.parentNode.nextElementSibling;
      // 显示回复输入
      if (replyDom.style.display === "" || replyDom.style.display === "none") {
        replyDom.style.display = "flex";
        var replyInput = replyDom.getElementsByTagName("input")[0];
        // 添加回复人信息
        var placeContent = "回复" + " @ " + obj.senderName;
        replyInput.setAttribute("placeholder", placeContent);
      } else {
        replyDom.style.display = "none";
      }
    },
    // 回复信息提交
    replySumbit(obj, event) {
      var _thisDom = event.currentTarget;
      var replyDom = _thisDom.parentNode;
      var _this = this;

      // 回复内容非空验证
      if (!_this.replyComment || _this.replyComment === "") {
        _this.hasNoConent = true;
        _this.inputStatusClass = "no-content-warn";
        return;
      }
      _this.inputStatusClass = "";
      var reply = {
        objectId: obj.objectId,
        parentId: obj.id,
        type: "Class",
        text: _this.replyComment,
        senderId: obj.userId,
        receiverId: obj.senderId,
        senderAvatar: "",
        children: [],
        receiverName: obj.senderName,
        senderName: obj.senderName,
        objectId: obj.objectId,
        receiverId: obj.receiverId,
        senderId: obj.senderId,
        postDate: obj.postDate,
        id: obj.id,
        likes: 0
      };
      // 更新回复数组,将新回复添加到数组尾部
      _this.$set(obj.children, obj.children.length, reply);
      // 清空内容
      _this.replyComment = "";
      replyDom.style.display = "none";
    },
  },
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
/* 评论内容区域 */
.msg-content .comments-child {
  display: flex;
  position: relative;
  padding: 18px 10px 18px 10px;
}
.comments-child-contain {
  border-bottom: 1px solid #d3d9e1;
  padding: 0 25px;
}

/* 子评论头像 */
.comments-child .comments-child-img {
  /*flex: 1;*/
  text-align: center;
  padding: 0 20px 0 0;
}
/* 子评论头像 */
.comments-child-img > img {
  width: 50px;
  height: 50px;
  border-radius: 50%;
}

/* 子评论小头像 */
.comments-child .comments-child-img-sm {
  /*flex: 1;*/
  text-align: center;
  padding: 0 20px 0 0;
}
/* 子评论小头像 */
.comments-child-img-sm > img {
  width: 35px;
  height: 35px;
  border-radius: 50%;
}

/* 子评论用户名 */
.comments-child-username {
  color: #504f4f;
  margin: 0;
  font-size: 15px;
  width: auto;
  text-align: left;
}

/* 子评论回复用户名 */
.comments-child-at-username {
  margin: 0;
  color: #00a1d6;
}

.comments-child-username-contain {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  flex-wrap: nowrap;
  /*margin-bottom: 15px;*/
}

/* 回复内容 */
.reply-text {
  margin: 0 10px;
  font-size: 16px;
  font-weight: 400;
  color: #000 !important;
  font-family: "Lato", Verdana, sans-serif !important;
}

.comments-child-replay {
  display: flex;
  align-items: center;
  font-size: 15px;
  margin: 0;
}

.comments-child-content {
  flex: 9;
}
/* 回复时间 */
.comments-child-time {
  color: #767575;
  font-size: 12px;
  white-space: nowrap;
}
.comments-comments-child {
  font-size: 16px;
  margin-top: 10px;
  margin-bottom: 10px;
  font-weight: 400;
  color: #000 !important;
  font-family: "Lato", Verdana, sans-serif !important;
  text-align: left;
}

.comments-child-bottom-contain {
  display: flex;
  align-items: center;
}
/* 右边点赞和评论 */
.comments-child-right {
  position: absolute;
  right: 1.5%;
  top: 10px;
  white-space: nowrap;
}
.comments-child-right span {
  font-weight: 400;
  font-size: 15px;
  margin: 0 20px;
  cursor: pointer;
  color: #333 !important;
}
/* 删除评论 */
.delete:hover {
  color: red;
}
.delete::before {
  /* 想使用的icon的十六制编码,去掉&#x之后的 */
  margin-right: 4px;
  font-size: 16px;
}
/* 评论字体图标 */
.comments::before {
  /* 想使用的icon的十六制编码,去掉&#x之后的 */
  margin-right: 4px;
  font-size: 16px;
}
/* 点赞字体图标 */
.praise::before {
  /* 想使用的icon的十六制编码,去掉&#x之后的 */
  content: "\ec7f";
  /* 必须加 */
  font-family: "iconfont";
  margin-right: 4px;
  font-size: 19px;
}

.to_reply {
  color: rgb(106, 106, 106);
}

/* 评论 */
.reply-comment {
  margin: 10px 0 0 0;
  align-items: center;
  justify-content: space-around;
  display: none;
}
/* 评论输入框头像 */
.reply-comment > img {
  width: 50px;
  height: 50px;
  border-radius: 50%;
}
/* 评论输入框 */
.reply-comment input {
  height: 40px;
  border-radius: 5px;
  outline: none;
  width: 70%;
  font-size: 16px;
  padding: 0 10px;
  /* border: 2px solid #f8f8f8; */
  border: 2px solid skyblue;
}
/* 发布评论按钮 */
.reply-comment button {
  width: 100px;
  height: 43px;
  border: 0;
  border-radius: 5px;
  font-size: 16px;
  font-weight: 500;
  letter-spacing: 2px;
  color: #fff !important;
  background-color: #00a1d6;
  cursor: pointer;
}
/* 鼠标经过字体加粗 */
.reply-comment button:hover {
}
/* 评论点赞颜色 */
.comment-like {
  color: red;
}

.no-content-warn {
  border: 1px solid red !important;
}

@media (max-width: 900px) {
  .comments-child-right {
    position: inherit;
    margin-left: 10px;
  }

  .comments-child > img {
    width: 40px;
    height: 40px;
    border-radius: 50%;
  }

  .reply-comment button {
    width: 50px;
    height: 43px;
    border: 0;
    border-radius: 5px;
    font-size: 14px;
    font-weight: 500;
    color: #fff !important;
    background-color: #00a1d6;
    cursor: pointer;
  }

  .reply-comment input {
    height: 40px;
    border-radius: 5px;
    outline: none;
    width: 50%;
    font-size: 16px;
    padding: 0 10px;
    margin: 0 10px;
    /* border: 2px solid #f8f8f8; */
    border: 2px solid skyblue;
  }

  .comments-child-right span {
    font-weight: 400;
    font-size: 12px;
    margin: 0 5px;
    cursor: pointer;
    color: #333 !important;
  }

  .reply-comment {
    justify-content: flex-start;
  }
  .container-fluid {
    position: relative;
  }

  .comments-child-username-contain {
    flex-wrap: wrap;
  }
  .comments-child-username {
    width: 100%;
  }
  .comments-child-replay {
    margin-top: 10px;
  }
  .reply-text {
    margin: 0 10px 0 0;
  }
  .msg-class {
    font-size: 25px;
    line-height: 26px;
  }
}
</style>


主要注意点是:

  1. 递归组件
  2. 通过count检测子组件递归次数
  3. 接口返回的字段

项目代码在下面
项目地址:https://gitee.com/joeyan3/joe-vue-demo-project.git
【vue组件】简单留言板 可回复 可点赞 无@_第3张图片

你可能感兴趣的:(vue开发实践总结,vue.js,javascript,前端)