vue项目封装评论组件

本文介绍如何在Vue项目中使用ElementUI封装一个评论组件。通过接口获取数据后进行处理,将评论列表(commentList)赋值展示,实现静态效果。

效果图:

评论组件:

<template>
  <div class="comment-view">
    <div v-show="isShowInput" id="commentForm" class="comment-form white-box">
      <div class="form-box">
        <el-input
          ref="commentInput"
          v-model="content"
          type="textarea"
          :rows="3"
          :autofocus="true"
          resize="none"
          placeholder="请输入内容"
        ></el-input>
        <el-button :loading="isLoading" :disabled="isLoading" class="primary" size="small" @click="commentSubmit">发布</el-button>
      </div>
    </div>
    <div ref="commentCt" class="comment-ct white-box">
      <div class="tit">
        评论<span v-if="count">({{ count }})</span>
      </div>
      <div class="comment-list">
        <div v-for="(item, index) in commentList" :key="index" class="item">
          <a class="left-avatar" :href="`/user/${item.memberId}`">
            <el-image :src="item.avatar" fit="cover" class="avatar"></el-image>
          </a>
          <div class="right-info">
            <div class="person">
              <div class="name-time">
                <a class="name" :href="`/user/${item.memberId}`">{{ item.nickname }}</a>
                <SvgIcon v-if="item.isExpert === 1" icon="auth"></SvgIcon>
                <el-tag v-if="item.isAuthor === 1" color="#F3F3F3" type="info" size="mini">作者</el-tag>
                <div class="time">{{ item.releaseTime | dateFormat }}</div>
              </div>
            </div>
            <div class="info">{{ item.content }}</div>
            <ItemOperation
              :operStaus="[
                { icon: 'o-nice', key: 'giveCount', isOperate: true },
                { icon: 'o-comment', text: '回复', isOperate: true },
                { icon: 'o-report', text: '举报', isOperate: true },
              ]"
              :data="item"
              :type="5"
              @handleComment="handleComment"
            >
            </ItemOperation>
            <div v-if="formId === item.id" class="item-form-box">
              <el-input v-model="formValue" :autofocus="true" :placeholder="formPlaceholder"></el-input>
              <el-button
                class="primary"
                size="small"
                :loading="isLoading"
                :disabled="isLoading"
                @click="replySubmit(item.id, item.memberId, 1)"
              >发布</el-button>
            </div>
            <div class="children-list">
              <div
                v-for="(it, i) in item.children"
                v-show="i < showCount || openIds.includes(item.id)"
                :key="i"
                class="item"
              >
                <a class="left-avatar" :href="`/user/${it.memberId}`">
                  <el-image :src="it.avatar" fit="cover" class="avatar"></el-image>
                </a>
                <div class="right-info">
                  <div class="person">
                    <div class="name-time">
                      <a class="name" :href="`/user/${it.memberId}`">{{ it.nickname }}</a>
                      <SvgIcon v-if="it.isExpert === 1" icon="auth"></SvgIcon>
                      <el-tag v-if="it.isAuthor === 1" color="#F3F3F3" type="info" size="mini">作者</el-tag>
                      <div v-if="it.beNickname" class="reply"><span>回复</span> {{ it.beNickname }}</div>
                      <div class="time">{{ it.releaseTime | dateFormat }}</div>
                    </div>
                  </div>
                  <div class="info">{{ it.content }}</div>
                  <ItemOperation
                    :operStaus="[
                      { icon: 'o-nice', key: 'giveCount', isOperate: true },
                      { icon: 'o-comment', text: '回复', isOperate: true },
                      { icon: 'o-report', text: '举报', isOperate: true },
                    ]"
                    :data="it"
                    :type="5"
                    @handleComment="handleComment"
                  />
                  <div v-if="formId === it.id" class="item-form-box">
                    <el-input v-model="formValue" :autofocus="true" :placeholder="formPlaceholder"></el-input>
                    <el-button
                      class="primary"
                      size="small"
                      :loading="isLoading"
                      :disabled="isLoading"
                      @click="replySubmit(item.id, it.memberId, 2)"
                    >发布</el-button>
                  </div>
                </div>
              </div>
              <el-link
                v-if="item.children.length > showCount && !openIds.includes(item.id)"
                class="link"
                type="primary"
                :underline="false"
                @click="openCommit(item)"
              >展开{{ item.children.length - showCount }}条评论</el-link>
            </div>
          </div>
        </div>
      </div>
      <Empty :type="loadingType" @loading="loading" />
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import { controlComment, getCommentList } from "@api/public/index";
import { commentItem } from "@api/public/index.d";

interface DomEle extends Element {
  offsetTop: number;
}
@Component({
  name: "CommentView",
  components: {},
})
export default class CommentView extends Vue {
  @Prop({
    type: Number,
    default: 0,
    required: true,
  })
  readonly sourceId!: number; // 来源文章id
  openIds: number[] = [];
  isLoading = false;
  showCount = 1; // 控制第一次加载几条回复
  content = ""; // 发布评论输入框内容
  formValue = ""; // 回复评论输入框内容,控制评论中的评论框内容
  formId: number | string = ""; // 控制评论中的评论框的展示
  formPlaceholder = ""; // 回复输入框的提示语
  commentList: commentItem[] = [];
  listQuery = {
    page: 1,
    limit: 5,
  };
  count = 0;
  loadingType = 0;
  isShowInput = false;
  // 滚动到评论输入框并聚焦
  pageScrollFn() {
    this.isShowInput = !this.isShowInput;
    if (this.isShowInput) {
      this.content = "";
      this.formId = "";
      this.$nextTick(() => {
        const formDom: DomEle | null = document.querySelector("#commentForm");
        formDom &&
          window.scrollTo({
            top: formDom.offsetTop - 160,
            behavior: "smooth",
          });
        (this.$refs.commentInput as any).focus();
      });
    }
  }
  // 滚动到评论
  scrollComment() {
    this.$nextTick(() => {
      const formDom: any = this.$refs.commentCt;
      formDom &&
        window.scrollTo({
          top: formDom.offsetTop - 80,
          behavior: "smooth",
        });
    });
  }
  // 展示回复输入框并清空值
  handleComment(info: any) {
    if (this.formId === info.id) {
      this.formId = "";
    } else {
      this.formId = info.id;
    }
    this.formValue = "";
    this.formPlaceholder = `回复:${info.nickname}`;
  }
  // 控制显示回复数量
  openCommit(item: { id: number }) {
    const i = this.openIds.findIndex(x => x === item.id);
    if (i !== -1) {
      this.openIds.splice(i, 1);
    } else {
      this.openIds.push(item.id);
    }
  }
  /**
   * @description:  发布评论/回答
   * @param {*}
   * @return {*}
   */
  async commentSubmit() {
    const nowPath = this.$route.fullPath;
    await this.$loginDialog({ nowPath });
    if (!this.content) {
      this.$message("请填写评论内容");
      return;
    }
    this.isLoading = true;
    const json = {
      content: this.content,
      pid: 0,
      sourceId: this.sourceId,
    };
    try {
      await controlComment(json);
      this.isLoading = false;
      this.content = "";
      this.listQuery.page = 1;
      this.getCommentList();
      this.$emit("refresh");
    } catch (error) {
      this.isLoading = false;
    }
  }
  /**
   * @description:  回复
   * @param {*} pid  一级评论id
   * @param {*} beMemberId  被评论用户id
   * @param {*} type  1 回复评论   回复回复
   * @return {*}
   */
  async replySubmit(pid: number, beMemberId: number, type: number) {
    const nowPath = this.$route.fullPath;
    await this.$loginDialog({ nowPath });
    if (!this.formValue) {
      this.$message("请填写评论内容");
      return;
    }
    this.isLoading = true;
    const json: any = {
      content: this.formValue,
      pid,
      sourceId: this.sourceId,
    };
    if (type === 2) {
      json.beMemberId = beMemberId;
    }
    try {
      await controlComment(json);
      this.isLoading = false;
      this.formId = "";
      this.formValue = "";
      const {
        data: { list, count },
      } = await getCommentList({ sourceId: this.sourceId, page: 1, limit: this.commentList.length });
      this.commentList = list;
      this.count = count;
      const i = this.openIds.findIndex(x => x === pid);
      if (i === -1) {
        this.openIds.push(pid);
      }
      this.$emit("refresh");
    } catch (error) {
      this.isLoading = false;
    }
  }
  /**
   * @description:  获取案例/知识评论列表
   * @return {*}
   */
  async getCommentList() {
    this.loadingType = 1;
    const {
      data: { list, count },
    } = await getCommentList({ sourceId: this.sourceId, ...this.listQuery });
    if (count === 0) {
      this.loadingType = 3;
    } else if (this.listQuery.page * this.listQuery.limit >= count) {
      this.loadingType = 2;
    } else {
      this.loadingType = 0;
    }
    this.count = count;
    if (this.listQuery.page === 1) {
      this.commentList = list;
    } else {
      this.commentList = [...this.commentList, ...list];
    }
  }
  /**
   * @description: 加载更多
   * @return {*}
   */
  loading() {
    this.listQuery.page++;
    this.getCommentList();
  }
}
</script>
<style scoped lang="scss">
.comment-view {
  .form-box {
    background: #ecf0ff;
    text-align: right;
    border-radius: 4px;
    padding: 15px;
    /deep/ .el-textarea {
      .el-textarea__inner {
        background: transparent;
        border: 0;
        font-size: 14px;
        color: #000;
        height: 80px;
        padding: 0;
      }
    }
    .el-button {
      margin-top: 15px;
    }
  }
  .item-form-box {
    margin-top: 15px;
    display: flex;
    align-items: center;
    background: #ecf0ff;
    border-radius: 4px;
    padding: 12px;
    /deep/ .el-input {
      &__inner {
        background: #fff;
        border: 0;
        font-size: 14px;
        color: #000;
        padding: 0 12px;
      }
    }
    .el-button {
      margin-left: 12px;
    }
  }
  .comment-form {
    margin-top: 20px;
  }
  .comment-ct {
    margin-top: 20px;
    .tit {
      position: relative;
      padding-left: 20px;
      font-size: 24px;
      font-weight: 600;
      @media (max-width: 767px) {
        font-size: 20px;
      }
      @media (min-width: 768px) and (max-width: 991px) {
        font-size: 22px;
      }
      &::before {
        content: "";
        position: absolute;
        top: 50%;
        left: 0;
        transform: translateY(-50%);
        width: 6px;
        height: 100%;
        background: #1347f2;
      }
    }
    .comment-list {
      .item {
        display: flex;
        margin-top: 18px;
        .left-avatar {
          flex: 0 0 40px;
          height: 40px;
          margin-right: 8px;
          cursor: pointer;
        }
        .right-info {
          flex: 1;
          &::after {
            content: "";
            display: block;
            height: 1px;
            background: #e4eaff;
            margin-top: 18px;
          }
          .person {
            // line-height: 30px;
            .name-time {
              display: flex;
              align-items: center;
              .name {
                text-decoration: none;
                color: #333;
                font-size: 16px;
                font-weight: bold;
                margin-right: 4px;
                cursor: pointer;
              }
              /deep/ .svg-icon {
                width: 16px;
                height: 16px;
              }
              /deep/.el-tag {
                // margin: 0 8px;
                margin-left: 8px;
              }
              .reply {
                font-size: 14px;
                color: #333333;
                span {
                  color: #cccccc;
                  margin: 0 6px;
                }
              }
              .time {
                font-size: 14px;
                color: #ccc;
                padding-left: 10px;
              }
            }
          }
          .info {
            margin: 5px 0 12px;
            font-size: 16px;
            color: #707070;
          }
          /deep/ .item-operation {
            .tag {
              padding: 1px 8px;
              border-radius: 4px;
              border: 1px solid #1347f2;
              font-size: 14px;
              color: #1347f2;
              @media (max-width: 1199px) {
                font-size: 12px;
              }
              &.active {
                border: 1px solid #52c41a;
                color: #52c41a;
              }
            }
          }
          .form-box {
            margin-top: 10px;
          }
        }
      }
    }
    .children-list {
      .item {
        .right-info {
          &::after {
            display: none;
          }
        }
        .person {
          line-height: 22px;
        }
        .info {
          margin: 10px 0 10px;
        }
        .left-avatar {
          flex: 0 0 26px;
          height: 26px;
          margin-right: 5px;
          cursor: pointer;
        }
      }
      .link {
        margin-top: 10px;
      }
    }
    .btn-box {
      margin-top: 20px;
      .el-button {
        width: 100%;
      }
    }
  }
}
</style>

接口数据处理:可直接赋值给commentList看静态效果

[
  {
    id: 37,
    pid: 0,
    memberId: 100020,
    content: "写的太烂了。。。",
    beMemberId: 100014,
    beNickname: "cz6",
    releaseTime: "2022-06-14 11:25",
    nickname: "xxx名字",
    avatar:
      "https://xchlwkj.oss-cn-shenzhen.aliyuncs.com/2022/05/13/member-default-avatar.png.png",
    isExpert: 1,
    isAuthor: 2,
    isGive: 2,
    giveCount: 0,
    level: 1,
    children: [
      {
        id: 66,
        pid: 37,
        memberId: 100014,
        content: "瞎说什么大实话",
        beMemberId: 100020,
        beNickname: "xxx名字",
        releaseTime: "1小时前",
        nickname: "cz6",
        avatar:
          "https://thirdwx.qlogo.cn/mmopen/vi_32/mtPRUG9a4Ond75av9Rb0kibxLwNebGxeveCFM3M4v4XE59NIHYAPGkquld3mQ18QWic08MOrzQhBPZibMlLrTBGsQ/132",
        isExpert: 1,
        isAuthor: 1,
        isGive: 2,
        giveCount: 0,
        level: 2,
        children: [],
      },
      {
        id: 55,
        pid: 37,
        memberId: 100014,
        content: "xxx0614编辑",
        beMemberId: 100014,
        beNickname: "cz6",
        releaseTime: "2022-06-14 16:06",
        nickname: "cz6",
        avatar:
          "https://thirdwx.qlogo.cn/mmopen/vi_32/mtPRUG9a4Ond75av9Rb0kibxLwNebGxeveCFM3M4v4XE59NIHYAPGkquld3mQ18QWic08MOrzQhBPZibMlLrTBGsQ/132",
        isExpert: 1,
        isAuthor: 1,
        isGive: 2,
        giveCount: 0,
        level: 2,
        children: [],
      },
    ],
  },
  {
    id: 36,
    pid: 0,
    memberId: 100014,
    content: "写的不错",
    beMemberId: 100014,
    beNickname: "cz6",
    releaseTime: "2022-06-14 11:22",
    nickname: "cz6",
    avatar:
      "https://thirdwx.qlogo.cn/mmopen/vi_32/mtPRUG9a4Ond75av9Rb0kibxLwNebGxeveCFM3M4v4XE59NIHYAPGkquld3mQ18QWic08MOrzQhBPZibMlLrTBGsQ/132",
    isExpert: 1,
    isAuthor: 1,
    isGive: 1,
    giveCount: 1,
    level: 1,
    children: [
      {
        id: 41,
        pid: 36,
        memberId: 100012,
        content: "cz6YYDS",
        beMemberId: 100014,
        beNickname: "cz6",
        releaseTime: "2022-06-14 11:35",
        nickname: "不可思议",
        avatar:
          "https://xchlwkj.oss-cn-shenzhen.aliyuncs.com/2022/05/13/member-default-avatar.png.png",
        isExpert: 2,
        isAuthor: 2,
        isGive: 2,
        giveCount: 0,
        level: 2,
        children: [],
      },
    ],
  },
];

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值