【Vue】高仿CSDN的评论区功能,全手敲!

1、效果

在这里插入图片描述
体验地址(服务器已过期):桂林高校社区

2、实现流程

2-1、当点击评论图标时,在右边弹出评论区,给该评论区设置一个div浮动标签,用v-if判断显示与否,本来想用el-drawer组件实现右边弹窗抽屉效果,但是那个遮罩层让我很不满,干脆自己实现一个抽屉效果。
    <!--评论弹窗-->
    <div v-if="drawer" class="commentDrawer">
      <span>评论区</span>
      <span @click="drawer = false" style="cursor: pointer;float: left;margin-left: 10px">关闭</span>
      <!--输入框-->
      <div class="inputComment">
        <textarea v-model="textareaContent" placeholder="说点什么..." maxlength="200" @input="calcInput">
        </textarea>
        <span style="font-size: 14px;float: left">还可以输入{{ canInputText }}个字符</span>
        <span class="sent" @click="sentComment">发送</span>
      </div>
      <!--评论列表-->
      <div class="comment-list">
        <ul class="comment-ul">
          <li class="comment-li" v-for="(com,index) in comments">
            <div class="comment-li-top">
              <img :src="com.avatarUrl"/>
              <span class="comment-nickName">{{ com.nickName }}</span>
              <span style="font-size: 15px;margin-left: auto">{{ com.commentTimeRes }}</span>
              <span v-if="!com.openReply"
                    style="cursor: pointer;margin-left:auto;font-size: 16px" @click="openReply(com,index)">
                回复</span>
              <span v-if="com.openReply"
                    style="cursor: pointer;margin-left: auto;font-size: 16px" @click="closeReply(index)">
                收起</span>
            </div>

            <div class="comment-li-content" @click="openReply(com,index)">
              <span style="text-align: left;float: left;cursor: pointer">{{ com.content }}</span>
            </div>
            <div class="inputReply" v-if="com.openReply">
              <textarea v-model="com.replyContent" :placeholder="com.placeholder" maxlength="200"
                        @input="calcInputReply(index)">
              </textarea>
              <span style="font-size: 14px;float: left">还可以输入{{ com.canInputReply }}个字符</span>
              <span class="sent" @click="sentReply(com,index)">回复</span>
            </div>
            <!--回复列表-->
            <ul class="reply-ul">
              <li class="reply-li" v-for="(reply,index1) in com.replyies">
                <div class="reply-li-top">
                  <img :src="reply.fromUserAvatarUrl"/>
                  <span class="reply-nickName">{{ reply.fromUserNickName }}
                    <span style="font-size: 15px;margin: 3px;color: var(--text-color)">回复</span>
                    {{reply.toUserNickName}}</span>
                  <span style="margin-left: auto;font-size: 13px">{{ reply.replyTimeRes }}</span>
                  <span v-if="!reply.openReply"
                        style="cursor: pointer;margin-left:auto;font-size: 14px" @click="openReplySon(reply)">
                  回复</span>
                  <span v-if="reply.openReply"
                        style="cursor: pointer;margin-left: auto;font-size: 14px" @click="closeReplySon(reply)">
                  收起</span>
                </div>
                <div class="reply-li-content" @click="openReplySon(reply)">
                  <span style="float: left">{{ reply.content }}</span>
                </div>
                <!--回复下的回复框-->
                <div class="inputReplySon" v-show="reply.openReply">
                  <textarea v-model="reply.replyContent" :placeholder="reply.placeholder" maxlength="200"
                        @input="calcReplySon(index,index1)">
                  </textarea>
                  <span style="font-size: 12px;float: left">还可以输入{{ reply.canInputReply }}个字符</span>
                  <span class="sent" @click="sentReplySon(com,reply)">回复</span>
                </div>
              </li>
            </ul>
          </li>
        </ul>
      </div>
    </div>
2-2、点击评论图标的时候,触发一个方法,打开弹窗,并根据文章id请求后台评论,后端用Java对评论和回复进行封装返回
    //跳转评论详情
    toComment(id) {
      this.drawer = false
      this.clickArticleId = id
      setTimeout(() => {
        this.drawer = true
      }, 100)
      //获取该动态所有评论
      this.$http.post('/circle/comment/getComment', {
        articleId: id,
        type: 'article'
      }).then(res => {
        console.log('评论', res)
        this.comments = res.data.comments
      })
    },
2-3、发表评论,将评论内容和用户id以及文章id发给后端保存,发表的时候,前端伪更新评论区,将新发表的评论push到comments里面,就不需要再次请求后端获取所有评论,除非用户主动刷新页面。
   //发表评论
    sentComment() {
      if (this.userInfo === null) {
        //没有登录
        this.$message({
          message: '请先登陆哦~',
          type: 'warning'
        })
        this.$loginToast.open()
        return
      }
      let articleId = this.clickArticleId
      let userId = this.userInfo.id
      let type = 'article'
      let content = this.textareaContent
      let comment = {
        type: type,
        composeId: articleId,
        content: content,
        fromUserid: userId
      }
      //请求
      this.$http.post('/circle/comment/addComment', comment).then(res => {
        if (res.data.code === 200) {
          this.$message({
            message: '发送成功',
            type: 'success'
          })
          this.textareaContent = ''
        } else {
          this.$message({
            message: '发送失败',
            type: 'error'
          })
        }
      })
      //用现有数据更新当前评论区
      let addComment = {
        avatarUrl: this.userInfo.avatarUrl,
        nickName: this.userInfo.nickName,
        content: this.textareaContent,
        replyies: [],
        openReply: false,
        commentTimeRes: '1秒前',
        placeholder: '',
        replyContent: '',
        canInputReply: 200,
      }
      this.comments.push(addComment)
    },
2-4、打开回复框,注意我们动态绑定了每条评论的openReply属性,每个回复框都是独立的,并不会相互影响,更新openReply属性不能简单地用this.comment[index].openReply = true ,这样打开并不能让页面重新渲染,要用this.$set(对象,属性,值)来动态渲染。
 //打开回复框
    openReply(com) {
      //打开该评论的回复框
      this.$set(com, 'openReply', true)
      this.$set(com, 'placeholder', '回复@' + com.nickName)
    },
    closeReply(index) {
      this.$set(this.comments[index], 'openReply', false)
    },
    //打开回复下面的回复框
    openReplySon(reply){
      this.$set(reply,'openReply',true)
      this.$set(reply,'placeholder','回复'+ reply.fromUserNickName)
    },
    closeReplySon(reply){
      this.$set(reply,'openReply',false)
    },
2-5、发送回复,动态更新评论区的回复区
    sentReply(com, index) {
      //先判断有没有登录
      if (this.userInfo === null) {
        //没有登录
        this.$message({
          message: '请先登陆哦~',
          type: 'warning'
        })
        this.$loginToast.open()
        return
      }
      let reply = {
        commentId: com.id,
        content: com.replyContent,
        replyTimeRes: '一秒前',
        fromUserid: this.userInfo.id,
        toUserid: com.fromUserid,
        fromUserAvatarUrl: this.userInfo.avatarUrl,
        fromUserNickName: this.userInfo.nickName,
        toUserNickName: com.nickName,
        canInputReply: 200,
        placeholder: '',
        replyContent: ''
      }
      //更新当前视图
      this.comments[index].replyies.push(reply)
      this.$http.post('/circle/reply/sentReply', reply).then(res => {
        console.log(res)
        if (res.data.code === 200){
          //关闭回复框
          this.$set(com,'replyContent','')
          this.$set(com,'openReply',false)
        }
      })
    },
2-6、评论区的样式表,回复区的样式比较多
.commentDrawer {
  background-color: var(--li-bg-color);
  color: var(--text-color);
  position: fixed;
  z-index: 2005;
  width: 30%;
  height: 100%;
  right: 0;
  top: 0;
  border-top-left-radius: 14px;
  border-bottom-left-radius: 14px;
}

.inputComment {
  margin-top: 5%;
  width: 88%;
  margin-left: 3%;
  position: relative;
  height: 185px;
  border-radius: 13px;
  padding: 3%;
  background-color: var(--main-bg-color);
  color: var(--text-color);
}

.inputComment textarea {
  width: 98%;
  position: relative;
  height: 150px;
  border: 0 solid;
  outline: none;
  resize: none;
  background-color: var(--main-bg-color);
  color: var(--text-color);
}

.inputComment .sent {
  border-radius: 30px;
  background-color: #ee464b;
  color: white;
  padding: 2px 15px;
  font-size: 16px;
  float: right;
  cursor: pointer;
  height: 25px;
}

.comment-list {
  width: 100%;
  border-radius: 14px;
  margin-top: 4%;
  height: 72%;
  position: relative;
}

.comment-ul {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 99%;
  position: relative;
  list-style-type: none;
  overflow: auto;
}

.comment-li {
  margin: 2%;
  float: left;
  width: 96%;
  position: relative;
  overflow: auto;
}

.comment-li-top {
  display: flex;
  float: left;
  align-items: center;
  width: 90%;
}

.comment-li-top span {
  margin-left: 8px;
}

.comment-li img {
  width: 35px;
  height: 35px;
  border-radius: 50%;
}

.comment-li-content {
  margin-left: 7%;
  width: 86%;
  font-size: 18px;
  clear: left;
  float: left;
  padding: 5px;
  cursor: pointer;
  overflow: auto;
}

.comment-nickName {
  font-size: 18px;
  color: #2073e3;
}

.inputReply {
  float: left;
  margin-top: 1%;
  width: 76%;
  margin-left: 8%;
  position: relative;
  height: 150px;
  border-radius: 13px;
  padding: 2%;
  background-color: var(--main-bg-color);
  color: var(--text-color);
}

.inputReply textarea {
  width: 98%;
  position: relative;
  height: 120px;
  border: 0 solid;
  outline: none;
  resize: none;
  background-color: var(--main-bg-color);
  color: var(--text-color);
}

.inputReply .sent {
  border-radius: 30px;
  background-color: #ee464b;
  color: white;
  padding: 2px 15px;
  font-size: 16px;
  float: right;
  cursor: pointer;
  height: 25px;
}
.reply-ul {
  margin: 0;
  padding: 0;
  width: 100%;
  position: relative;
  list-style-type: none;
  overflow: auto;
}

.reply-li {
  margin-left: 7%;
  margin-top: 1%;
  float: left;
  width: 92%;
  position: relative;
  overflow: auto;
}

.reply-li-top {
  display: flex;
  float: left;
  align-items: center;
  width: 90%;
}
.reply-li img {
  width: 35px;
  height: 35px;
  border-radius: 50%;
}

.reply-li-content {
  margin-left: 8%;
  width: 82%;
  font-size: 16px;
  clear: left;
  float: left;
  padding: 5px;
  cursor: pointer;
}

.reply-nickName {
  font-size: 16px;
  color: #2073e3;
}

.inputReplySon {
  float: left;
  margin-top: 1%;
  width: 69%;
  margin-left: 9%;
  position: relative;
  height: 150px;
  border-radius: 13px;
  padding: 2%;
  background-color: var(--main-bg-color);
  color: var(--text-color);
}

.inputReplySon textarea {
  width: 93%;
  position: relative;
  height: 120px;
  border: 0 solid;
  outline: none;
  resize: none;
  background-color: var(--main-bg-color);
  color: var(--text-color);
}

.inputReplySon .sent {
  border-radius: 30px;
  background-color: #ee464b;
  color: white;
  padding: 2px 13px;
  font-size: 13px;
  float: right;
  cursor: pointer;
  height: 22px;
}
2-7、整个页面代码,包括动态列表和评论区
<template>
  <div>
    <div class="main">
      <div>
        <el-button type="primary" @click="publish" style="position: fixed;right: 20%">发布动态</el-button>
        <ul class="list" v-infinite-scroll="load" style="overflow:auto" infinite-scroll-immediate="false"
            v-loading="loading">
          <li v-for="(item,index) in articles" class="list-item">
            <div class="list-top">
              <img :src="item.publisherAvatarUrl"/>
              <div style="margin-left: 6px;color: #2073e3;float: left">
                <span style="float: left">{{ item.publisherNickName }}</span>
                <div style="width: 200px"></div>
                <span style="float: left;font-size: 15px;align-items: center;display: flex;color: #a426ce">
                  {{ item.publisherSchool }}
                  <!--性别图标-->
                  <img v-if="item.publisherGender===1" style="width: 26px;height: 26px;margin-left: 5px"
                       src="https://guilinus.oss-cn-guangzhou.aliyuncs.com/%E6%80%A7%E5%88%AB_%E7%94%B7.png"/>
                  <img v-if="item.publisherGender===2" style="width: 26px;height: 26px;margin-left: 5px"
                       src="https://guilinus.oss-cn-guangzhou.aliyuncs.com/%E6%80%A7%E5%88%AB-%E5%A5%B3.png"/>
                </span>
              </div>
            </div>

            <div class="list-main">
              <div class="list-main-text">
                <p>{{ item.content }}</p>
              </div>
              <div class="list-main-img">
                <div v-for="img in item.imgUrls">
                  <el-image :src="img" fit="cover"
                            :preview-src-list="item.imgUrls" @click="addClick(item.id)"></el-image>
                </div>
              </div>
              <div style="width: 100%"></div>
            </div>

            <div class="list-bottom">
              <span style="font-size: 17px;text-align: left;">{{ item.publishTimeRes }}</span>
              <span>
                浏览{{ item.click }}次
              </span>
              <!--评论-->
              <span @click="toComment(item.id)">
                <img
                    src="https://guilinus.oss-cn-guangzhou.aliyuncs.com/icon/%E8%AF%84%E8%AE%BA%E5%8C%BA%20%281%29.png"/>
                <span style="font-size: 26px">{{ item.comment }}</span>
              </span>
              <!--点赞-->
              <span>
                <img v-if="!item.isLike" @click="doLike(item.id,index)"
                     src="https://guilinus.oss-cn-guangzhou.aliyuncs.com/icon/%E7%82%B9%E8%B5%9E%20%281%29.png"/>
                <img v-if="item.isLike" @click="cancelLike(item.id,index)"
                     src="https://guilinus.oss-cn-guangzhou.aliyuncs.com/icon/%E7%82%B9%E8%B5%9E_%E5%9D%97%20%281%29.png"/>
                <span style="font-size: 26px">{{ item.zan }}</span>
              </span>
            </div>
          </li>
        </ul>
      </div>
    </div>

    <publish-dia ref="dia"></publish-dia>
    <!--    <article-detail ref="detail"></article-detail>-->
    <!--评论弹窗-->
    <div v-if="drawer" class="commentDrawer">
      <span>评论区</span>
      <span @click="drawer = false" style="cursor: pointer;float: left;margin-left: 10px">关闭</span>
      <!--输入框-->
      <div class="inputComment">
        <textarea v-model="textareaContent" placeholder="说点什么..." maxlength="200" @input="calcInput">
        </textarea>
        <span style="font-size: 14px;float: left">还可以输入{{ canInputText }}个字符</span>
        <span class="sent" @click="sentComment">发送</span>
      </div>
      <!--评论列表-->
      <div class="comment-list">
        <ul class="comment-ul">
          <li class="comment-li" v-for="(com,index) in comments">
            <div class="comment-li-top">
              <img :src="com.avatarUrl"/>
              <span class="comment-nickName">{{ com.nickName }}</span>
              <span style="font-size: 15px;margin-left: auto">{{ com.commentTimeRes }}</span>
              <span v-if="!com.openReply"
                    style="cursor: pointer;margin-left:auto;font-size: 16px" @click="openReply(com,index)">
                回复</span>
              <span v-if="com.openReply"
                    style="cursor: pointer;margin-left: auto;font-size: 16px" @click="closeReply(index)">
                收起</span>
            </div>

            <div class="comment-li-content" @click="openReply(com,index)">
              <span style="text-align: left;float: left;cursor: pointer">{{ com.content }}</span>
            </div>
            <div class="inputReply" v-if="com.openReply">
              <textarea v-model="com.replyContent" :placeholder="com.placeholder" maxlength="200"
                        @input="calcInputReply(index)">
              </textarea>
              <span style="font-size: 14px;float: left">还可以输入{{ com.canInputReply }}个字符</span>
              <span class="sent" @click="sentReply(com,index)">回复</span>
            </div>
            <!--回复列表-->
            <ul class="reply-ul">
              <li class="reply-li" v-for="(reply,index1) in com.replyies">
                <div class="reply-li-top">
                  <img :src="reply.fromUserAvatarUrl"/>
                  <span class="reply-nickName">{{ reply.fromUserNickName }}
                    <span style="font-size: 15px;margin: 3px;color: var(--text-color)">回复</span>
                    {{reply.toUserNickName}}</span>
                  <span style="margin-left: auto;font-size: 13px">{{ reply.replyTimeRes }}</span>
                  <span v-if="!reply.openReply"
                        style="cursor: pointer;margin-left:auto;font-size: 14px" @click="openReplySon(reply)">
                  回复</span>
                  <span v-if="reply.openReply"
                        style="cursor: pointer;margin-left: auto;font-size: 14px" @click="closeReplySon(reply)">
                  收起</span>
                </div>
                <div class="reply-li-content" @click="openReplySon(reply)">
                  <span style="float: left">{{ reply.content }}</span>
                </div>
                <!--回复下的回复框-->
                <div class="inputReplySon" v-show="reply.openReply">
                  <textarea v-model="reply.replyContent" :placeholder="reply.placeholder" maxlength="200"
                        @input="calcReplySon(index,index1)">
                  </textarea>
                  <span style="font-size: 12px;float: left">还可以输入{{ reply.canInputReply }}个字符</span>
                  <span class="sent" @click="sentReplySon(com,reply)">回复</span>
                </div>
              </li>
            </ul>
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script>
import publishDia from "@/views/gaoxiaoquan/publishDia";


export default {
  components: {
    publishDia,
  },
  name: "UniversityCircle",
  data() {
    return {
      articles: [],
      page: 0,
      limit: 20,
      loading: false,
      userInfo: {},
      clickArticleId: 0,
      drawer: false,
      textareaContent: '',
      canInputText: 200,
      comments: [
        {
          id: '0',
          replyies: [{
            fromUserNickName: '',
            toUserNickName: '',
            fromUserAvatarUrl: '',
            content: '',
            replyTimeRes: '',
            commentId: '',
            fromUserid: '',
            toUserid: '',
            replyContent: '',
            placeholder: '',
            openReply: false,
            canInputReply: 200
          }],
          commentTimeRes: '',
          openReply: false,
          placeholder: '',
          replyContent: '',
          canInputReply: 200,
        }
      ]
    }
  },
  created() {
    let userStatus = JSON.parse(localStorage.getItem('userInfo'))
    if (userStatus != null) {
      this.userInfo = userStatus.user
    } else {
      this.userInfo = null
    }
    this.load()
  },
  methods: {
    publish() {
      if(this.userInfo === null){
        this.$message({
          message: '请先登录',
          type: 'warning'
        })
        this.$loginToast.open()
        return
      }
      this.$refs.dia.open()
    },
    load() {
      //分页获取数据
      if (this.articles.length % 20 === 0) {
        this.loading = true
        this.$http.post('/circle/article/getArticles',
            {
              page: this.page,
              limit: this.limit,
              userId: this.userInfo === null ? '0' : this.userInfo.id
            }).then(res => {
          if (res.data.code === 200) {
            for (let i = 0; i < res.data.articles.length; i++) {
              this.articles.push(res.data.articles[i])
            }
            if (res.data.articles.length >= 20) {
              this.page += 20;
            }
          }
          console.log('返回', res)
          this.loading = false
        })
      } else {
        this.$message({
          message: '我是有底线哒~',
          type: 'warning',
          offset: 900,
          center: true
        })
      }
    },
    //点赞
    doLike(articleId, index) {
      //先判断有没有登录
      if (this.userInfo === null) {
        //没有登录
        this.$message({
          message: '登陆后才能点赞哦~',
          type: 'warning'
        })
        this.$loginToast.open()
      } else {
        console.log('articleId', articleId)
        let userId = this.userInfo.id
        this.$http.post('circle/zan/doLike', {
          articleId: articleId,
          userId: userId
        }).then(res => {
          console.log(res)
          this.articles[index].zan++
          this.articles[index].isLike = true
        })
      }
    },
    //取消点赞
    cancelLike(articleId, index) {
      //先判断有没有登录
      if (this.userInfo === null) {
        //没有登录
        this.$message({
          message: '请先登陆哦~',
          type: 'warning'
        })
        this.$loginToast.open()
      } else {
        let userId = this.userInfo.id
        this.$http.post('circle/zan/cancelLike', {
          articleId: articleId,
          userId: userId
        }).then(res => {
          console.log(res)
          this.articles[index].zan--
          this.articles[index].isLike = false
        })
      }
    },
    //增加浏览量
    addClick(id){
      console.log('id',id)
      this.$http.post('/circle/article/addClick',{
        articleId: id
      })
    },

    //跳转评论详情
    toComment(id) {
      this.drawer = false
      this.clickArticleId = id
      setTimeout(() => {
        this.drawer = true
      }, 100)
      //获取该动态所有评论
      this.$http.post('/circle/comment/getComment', {
        articleId: id,
        type: 'article'
      }).then(res => {
        console.log('评论', res)
        this.comments = res.data.comments
      })
    },
    //计算输入字数
    calcInput() {
      let len = this.textareaContent.length
      this.canInputText = 200 - len;
    },
    calcInputReply(index) {
      let len = this.comments[index].replyContent.length
      this.$set(this.comments[index], 'canInputReply', 200 - len)
    },
    calcReplySon(index,index1){
      let len = this.comments[index].replyies[index1].replyContent.length
      this.$set(this.comments[index].replyies[index1],'canInputReply',200 -len)
    },
    //发表评论
    sentComment() {
      if (this.userInfo === null) {
        //没有登录
        this.$message({
          message: '请先登陆哦~',
          type: 'warning'
        })
        this.$loginToast.open()
        return
      }
      let articleId = this.clickArticleId
      let userId = this.userInfo.id
      let type = 'article'
      let content = this.textareaContent
      let comment = {
        type: type,
        composeId: articleId,
        content: content,
        fromUserid: userId
      }
      //请求
      this.$http.post('/circle/comment/addComment', comment).then(res => {
        if (res.data.code === 200) {
          this.$message({
            message: '发送成功',
            type: 'success'
          })
          this.textareaContent = ''
        } else {
          this.$message({
            message: '发送失败',
            type: 'error'
          })
        }
      })
      //用现有数据更新当前评论区
      let addComment = {
        avatarUrl: this.userInfo.avatarUrl,
        nickName: this.userInfo.nickName,
        content: this.textareaContent,
        replyies: [],
        openReply: false,
        commentTimeRes: '1秒前',
        placeholder: '',
        replyContent: '',
        canInputReply: 200,
      }
      this.comments.push(addComment)
    },
    //打开回复框
    openReply(com) {
      //打开该评论的回复框
      this.$set(com, 'openReply', true)
      this.$set(com, 'placeholder', '回复@' + com.nickName)
    },
    closeReply(index) {
      this.$set(this.comments[index], 'openReply', false)
    },
    //打开回复下面的回复框
    openReplySon(reply){
      this.$set(reply,'openReply',true)
      this.$set(reply,'placeholder','回复'+ reply.fromUserNickName)
    },
    closeReplySon(reply){
      this.$set(reply,'openReply',false)
    },
    sentReply(com, index) {
      //先判断有没有登录
      if (this.userInfo === null) {
        //没有登录
        this.$message({
          message: '请先登陆哦~',
          type: 'warning'
        })
        this.$loginToast.open()
        return
      }
      let reply = {
        commentId: com.id,
        content: com.replyContent,
        replyTimeRes: '一秒前',
        fromUserid: this.userInfo.id,
        toUserid: com.fromUserid,
        fromUserAvatarUrl: this.userInfo.avatarUrl,
        fromUserNickName: this.userInfo.nickName,
        toUserNickName: com.nickName,
        canInputReply: 200,
        placeholder: '',
        replyContent: ''
      }
      //更新当前视图
      this.comments[index].replyies.push(reply)
      this.$http.post('/circle/reply/sentReply', reply).then(res => {
        console.log(res)
        if (res.data.code === 200){
          //关闭回复框
          this.$set(com,'replyContent','')
          this.$set(com,'openReply',false)
        }
      })
    },
    //发送回复下面的回复
    sentReplySon(com,parentReply){
      //先判断有没有登录
      if (this.userInfo === null) {
        //没有登录
        this.$message({
          message: '请先登陆哦~',
          type: 'warning'
        })
        this.$loginToast.open()
        return
      }
      let reply = {
        commentId: com.id,
        content: parentReply.replyContent,
        replyTimeRes: '一秒前',
        fromUserid: this.userInfo.id,
        toUserid: parentReply.fromUserid,
        fromUserAvatarUrl: this.userInfo.avatarUrl,
        fromUserNickName: this.userInfo.nickName,
        toUserNickName: parentReply.fromUserNickName,
        canInputReply: 200,
        placeholder: '',
        replyContent: ''
      }
      com.replyies.push(reply)
      this.$http.post('/circle/reply/sentReply',reply).then(res => {
        console.log(res)
        if (res.data.code === 200){
          //关闭回复框
          this.$set(parentReply,'replyContent','')
          this.$set(parentReply,'openReply',false)
        }
      })
    }
  }
}
</script>

<style scoped>
.main {
  width: 70%;
  position: absolute;
  left: 15%;
  top: 4%;
  height: 950px;
  background-color: var(--main-bg-color);
  border-radius: 13px;
}

.list {
  margin-top: 0;
  margin-left: 15%;
  list-style-type: none;
  width: 83%;
  height: 940px;
}

.list-item {
  margin: 3px;
  color: var(--text-color);
  border-radius: 10px;
  padding: 5px;
  float: left;
  width: 90%;
}

.list-top img {
  width: 45px;
  height: 48px;
  border-radius: 50%;
  object-fit: fill;
}

.list-top {
  display: flex;
  align-items: center;
  width: 300px;
  flex-wrap: wrap;
}

.list-main {
  float: left;
  display: flex;
  flex-wrap: wrap;
}

.list-main-text {
  text-align: left;
  float: left;
  overflow: hidden;
  text-overflow: ellipsis;
  width: 700px;
  display: -webkit-box;
  -webkit-line-clamp: 6;
  -webkit-box-orient: vertical;
}

.list-main-img {
  margin-top: 6px;
  float: left;
  display: flex;
  flex-wrap: wrap;
  width: 600px;
}

.list-main-img .el-image {
  width: 195px;
  height: 195px;
  border-radius: 8px;
  margin: 2px;
}

.list-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  width: 600px;
  height: 40px;

}

.list-bottom img {
  width: 30px;
  height: 30px;
  margin-top: 5px;
}

.commentDrawer {
  background-color: var(--li-bg-color);
  color: var(--text-color);
  position: fixed;
  z-index: 2005;
  width: 30%;
  height: 100%;
  right: 0;
  top: 0;
  border-top-left-radius: 14px;
  border-bottom-left-radius: 14px;
}

.inputComment {
  margin-top: 5%;
  width: 88%;
  margin-left: 3%;
  position: relative;
  height: 185px;
  border-radius: 13px;
  padding: 3%;
  background-color: var(--main-bg-color);
  color: var(--text-color);
}

.inputComment textarea {
  width: 98%;
  position: relative;
  height: 150px;
  border: 0 solid;
  outline: none;
  resize: none;
  background-color: var(--main-bg-color);
  color: var(--text-color);
}

.inputComment .sent {
  border-radius: 30px;
  background-color: #ee464b;
  color: white;
  padding: 2px 15px;
  font-size: 16px;
  float: right;
  cursor: pointer;
  height: 25px;
}

.comment-list {
  width: 100%;
  border-radius: 14px;
  margin-top: 4%;
  height: 72%;
  position: relative;
}

.comment-ul {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 99%;
  position: relative;
  list-style-type: none;
  overflow: auto;
}

.comment-li {
  margin: 2%;
  float: left;
  width: 96%;
  position: relative;
  overflow: auto;
}

.comment-li-top {
  display: flex;
  float: left;
  align-items: center;
  width: 90%;
}

.comment-li-top span {
  margin-left: 8px;
}

.comment-li img {
  width: 35px;
  height: 35px;
  border-radius: 50%;
}

.comment-li-content {
  margin-left: 7%;
  width: 86%;
  font-size: 18px;
  clear: left;
  float: left;
  padding: 5px;
  cursor: pointer;
  overflow: auto;
}

.comment-nickName {
  font-size: 18px;
  color: #2073e3;
}

.inputReply {
  float: left;
  margin-top: 1%;
  width: 76%;
  margin-left: 8%;
  position: relative;
  height: 150px;
  border-radius: 13px;
  padding: 2%;
  background-color: var(--main-bg-color);
  color: var(--text-color);
}

.inputReply textarea {
  width: 98%;
  position: relative;
  height: 120px;
  border: 0 solid;
  outline: none;
  resize: none;
  background-color: var(--main-bg-color);
  color: var(--text-color);
}

.inputReply .sent {
  border-radius: 30px;
  background-color: #ee464b;
  color: white;
  padding: 2px 15px;
  font-size: 16px;
  float: right;
  cursor: pointer;
  height: 25px;
}
.reply-ul {
  margin: 0;
  padding: 0;
  width: 100%;
  position: relative;
  list-style-type: none;
  overflow: auto;
}

.reply-li {
  margin-left: 7%;
  margin-top: 1%;
  float: left;
  width: 92%;
  position: relative;
  overflow: auto;
}

.reply-li-top {
  display: flex;
  float: left;
  align-items: center;
  width: 90%;
}
.reply-li img {
  width: 35px;
  height: 35px;
  border-radius: 50%;
}

.reply-li-content {
  margin-left: 8%;
  width: 82%;
  font-size: 16px;
  clear: left;
  float: left;
  padding: 5px;
  cursor: pointer;
}

.reply-nickName {
  font-size: 16px;
  color: #2073e3;
}

.inputReplySon {
  float: left;
  margin-top: 1%;
  width: 69%;
  margin-left: 9%;
  position: relative;
  height: 150px;
  border-radius: 13px;
  padding: 2%;
  background-color: var(--main-bg-color);
  color: var(--text-color);
}

.inputReplySon textarea {
  width: 93%;
  position: relative;
  height: 120px;
  border: 0 solid;
  outline: none;
  resize: none;
  background-color: var(--main-bg-color);
  color: var(--text-color);
}

.inputReplySon .sent {
  border-radius: 30px;
  background-color: #ee464b;
  color: white;
  padding: 2px 13px;
  font-size: 13px;
  float: right;
  cursor: pointer;
  height: 22px;
}
</style>

3、后端代码,包括获取所有动态和动态评论区代码

3-1、与article 有关的代码
3-1-1、Controller类
package cn.us.guiyoucircle.controller;


import cn.hutool.core.lang.hash.Hash;
import cn.us.guiyoucircle.entity.Article;
import cn.us.guiyoucircle.mapper.ArticleMapper;
import cn.us.guiyoucircle.service.ArticleService;
import cn.us.guiyoucircle.util.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import org.springframework.stereotype.Controller;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author 李石林
 * @since 2022-08-20
 */
@RestController
@RequestMapping("//circle/article")
public class ArticleController {

    @Autowired(required = false)
    private ArticleMapper articleMapper;
    @Autowired
    private ArticleService articleService;
    @PostMapping("/publish")
    public R publish(@RequestBody Article article){
        boolean flag = articleService.publishArticle(article);
        if (!flag){
            return R.error();
        }
        return R.ok();
    }
    @PostMapping("/getArticles")
    public R getArticles(@RequestBody HashMap<String,Object> map){
        int page = (int) map.get("page");
        int limit = (int) map.get("limit");
        long userId =Long.parseLong((String) map.get("userId"));
        List<Article> articles =  articleService.getArticlePage(page,limit,userId);
        return R.ok().put("articles",articles);
    }
    @PostMapping("addClick")
    public R addClick(@RequestBody HashMap<String,String> map){
        long articleId = Long.parseLong(map.get("articleId"));
        this.articleMapper.addClick(articleId);
        return R.ok();
    }
    @PostMapping("/getUserArticle")
    public R getUserArticle(@RequestBody Map<String,String> map){
        long userId = Long.parseLong(map.get("userId"));
        List<Article> articles = this.articleService.getUserArticles(userId);
        return R.ok().put("articles",articles);
    }
    @PostMapping("/updateLock")
    public R updateLock(@RequestBody Map<String,String> map){
        boolean lock = Boolean.parseBoolean(map.get("lock"));
        int flag = lock? 1:0;
        long articleId = Long.parseLong(map.get("articleId"));
        int f = this.articleMapper.updateLock(flag,articleId);
        if(f==0){
            return R.error();
        }
        return R.ok();
    }
    @PostMapping("/deleteArticle")
    public R deleteArticle(@RequestBody Map<String,String> map){
        long articleId = Long.parseLong(map.get("articleId"));
        this.articleService.removeById(articleId);
        return R.ok();
    }
}

3-1-2、Server实现类
package cn.us.guiyoucircle.service.impl;

import cn.us.guiyoucircle.entity.Article;
import cn.us.guiyoucircle.entity.ArticleImg;
import cn.us.guiyoucircle.entity.User;
import cn.us.guiyoucircle.mapper.ArticleImgMapper;
import cn.us.guiyoucircle.mapper.ArticleMapper;
import cn.us.guiyoucircle.mapper.CommentMapper;
import cn.us.guiyoucircle.mapper.ZanMapper;
import cn.us.guiyoucircle.service.ArticleImgService;
import cn.us.guiyoucircle.service.ArticleService;
import cn.us.guiyoucircle.service.UserService;
import cn.us.guiyoucircle.util.DateTimeResult;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 李石林
 * @since 2022-08-20
 */
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {

    @Autowired(required = false)
    ArticleMapper articleMapper;
    @Autowired(required = false)
    ArticleImgMapper articleImgMapper;
    @Autowired
    UserService userService;
    @Autowired(required = false)
    CommentMapper commentMapper;
    @Autowired
    DateTimeResult dateTimeResult;
    @Autowired(required = false)
    ZanMapper zanMapper;
    @Autowired
    private ArticleImgService articleImgService;
    @Override
    public boolean publishArticle(Article article) {
        System.out.println(article);
        //取出文章的所以图片地址
        List<String> imgUrls = article.getImgUrls();
        System.out.println("图片"+imgUrls);
        //获取当前时间
        LocalDateTime publishTime = LocalDateTime.now();
        article.setPublishTime(publishTime);
        int f = this.baseMapper.insert(article);
        if (f != 0){
            //插入成功,存图片
            Long articleId = article.getId();
            List<ArticleImg> list = new ArrayList<>();
            if(!imgUrls.isEmpty()){
                for(String s:imgUrls){
                    ArticleImg img = new ArticleImg();
                    img.setArticleId(articleId);
                    img.setImgurl(s);
                    list.add(img);
                }
                boolean flag = this.articleImgService.saveBatch(list);
                return flag;
            }
            return true;
        }
        return false;
    }

    @Override
    public List<Article> getArticlePage(int page, int limit, long userId) {
        //分页获取
        List<Article> list = this.articleMapper.getArticlesPage(page,limit);
        return articleListRes(list,userId);
    }

    @Override
    public List<Article> getUserArticles(long userId) {
        //获取该用户的动态
        List<Article> articles = this.articleMapper.getUserArticles(userId);
        return articleListRes(articles,userId);
    }
    //封装一个返回动态的方法
    public List<Article> articleListRes(List<Article> list,long userId){
        List<Article> newList = new ArrayList<>();
        for(Article article : list){
            //查这篇动态的图片
            Long id = article.getId();
            Long publisherId = article.getPublisherId();
            List<String> imgUrls =  this.articleImgMapper.getImgs(id);
            article.setImgUrls(imgUrls);
            String publishTimeRes = dateTimeResult.getTime(article.getPublishTime());
            article.setPublishTimeRes(publishTimeRes);
            User user = userService.getById(publisherId);
            article.setPublisherNickName(user.getNickName());
            article.setPublisherAvatarUrl(user.getAvatarUrl());
            article.setPublisherSchool(user.getSchool());
            article.setPublisherGender(user.getGender());
            //查询这篇动态的点赞数
            long zan = (long) this.zanMapper.getArticleZan(id);
            //查询评论数
            long comment = (long) this.commentMapper.getCommentCount(id,"article");
            //查询该用户是否点赞了该文章
            Integer likeStatus = (Integer) this.zanMapper.isLike(id,userId);
            boolean isLike = (likeStatus != null && likeStatus == 1);
            article.setZan(zan);
            article.setComment(comment);
            article.setIsLike(isLike);
            newList.add(article);
        }
        return newList;
    }
}

3-2、与评论有关的代码
3-2-1、Controller类
package cn.us.guiyoucircle.controller;


import cn.us.guiyoucircle.entity.Comment;
import cn.us.guiyoucircle.service.CommentService;
import cn.us.guiyoucircle.util.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author 李石林
 * @since 2022-08-20
 */
@RestController
@RequestMapping("//circle/comment")
public class CommentController {

    @Autowired
    CommentService commentService;
    @PostMapping("/addComment")
    public R addComment(@RequestBody Comment comment){
        comment.setCommentTime(LocalDateTime.now());
        boolean f =  this.commentService.save(comment);
        if(!f){
            return R.error();
        }
        return R.ok();
    }
    @PostMapping("/getComment")
    public R getComment(@RequestBody Map<String,String> map){
        long articleId = Long.parseLong(map.get("articleId"));
        System.out.println(articleId);
        String type = map.get("type");
        List<Comment> comments =  this.commentService.getComment(articleId,type);
        return R.ok().put("comments",comments);
    }
}


3-2-2、server实现类
package cn.us.guiyoucircle.service.impl;

import cn.hutool.core.date.DateTime;
import cn.us.guiyoucircle.entity.Comment;
import cn.us.guiyoucircle.entity.Reply;
import cn.us.guiyoucircle.entity.User;
import cn.us.guiyoucircle.mapper.CommentMapper;
import cn.us.guiyoucircle.mapper.ReplyMapper;
import cn.us.guiyoucircle.service.CommentService;
import cn.us.guiyoucircle.service.UserService;
import cn.us.guiyoucircle.util.DateTimeResult;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 李石林
 * @since 2022-08-20
 */
@Service
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {

    @Autowired
    UserService userService;
    @Autowired(required = false)
    CommentMapper commentMapper;
    @Autowired(required = false)
    ReplyMapper replyMapper;
    @Autowired
    DateTimeResult dateTimeResult;
    @Override
    public List<Comment> getComment(long articleId, String type) {
        List<Comment> comments = commentMapper.getComment(type,articleId);
        List<Comment> commentsList = new ArrayList<>();
        //对每一条评论,查找该评论的所有回复
        for(Comment c: comments){
            long userId = c.getFromUserid();
            User user = userService.getById(userId);
            c.setAvatarUrl(user.getAvatarUrl());
            c.setNickName(user.getNickName());
            //处理评论时间
            String commentTimeRes = this.dateTimeResult.getTime(c.getCommentTime());
            c.setCommentTimeRes(commentTimeRes);
            c.setCanInputReply(200);
            long commentId = c.getId();
            //通过评论id区回复表找回复
            List<Reply> replies =  this.replyMapper.getReply(commentId);
            List<Reply> replyList = new ArrayList<>();
            //对每一条回复,设置头像和昵称
            for(Reply r: replies){
                long fromUserid = r.getFromUserid();
                long toUserid = r.getToUserid();
                String replyTimeRes = this.dateTimeResult.getTime(r.getReplyTime());
                r.setReplyTimeRes(replyTimeRes);
                User fromUser = this.userService.getById(fromUserid);
                User toUser = this.userService.getById(toUserid);
                r.setFromUserNickName(fromUser.getNickName());
                r.setToUserNickName(toUser.getNickName());
                r.setFromUserAvatarUrl(fromUser.getAvatarUrl());
                r.setCanInputReply(200);
                replyList.add(r);
            }
            c.setReplyies(replyList);
            //将封装好的回复添加到list集合
            commentsList.add(c);
        }
        //返回封装好的评论
        return commentsList;
    }

}

3-3、与回复有关的代码
3-3-1、Controller类
package cn.us.guiyoucircle.controller;

import cn.us.guiyoucircle.entity.Reply;
import cn.us.guiyoucircle.service.ReplyService;
import cn.us.guiyoucircle.util.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author 李石林
 * @since 2022-08-20
 */
@RestController
@RequestMapping("//circle/reply")
public class ReplyController {

    @Autowired
    private ReplyService replyService;

    @PostMapping("/sentReply")
    public R sentReply(@RequestBody Reply reply){
        reply.setReplyTime(LocalDateTime.now());
        boolean f = this.replyService.save(reply);
        if (f){
            return R.ok();
        }
        return R.error();
    }
}


就这样。

  • 13
    点赞
  • 75
    收藏
    觉得还不错? 一键收藏
  • 22
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值