vue+elementUI+后端springboot多用户评论、回复、点赞

vue+elementUI+后端springboot多用户评论、回复、点赞

    • 演示
    • 数据库
    • 后端

评论、回复、点赞、取消点赞。
前端 vue+elementUI、vue脚手架vue-VLI、axios
后端 springboot、mybatis-plus(和其代码生成器)
数据库 MySQL
项目地址:https://gitee.com/panqiyi/comment-demo

演示

评论:
vue+elementUI+后端springboot多用户评论、回复、点赞_第1张图片

回复:
vue+elementUI+后端springboot多用户评论、回复、点赞_第2张图片

点赞、取消点赞(图标颜色变化):
vue+elementUI+后端springboot多用户评论、回复、点赞_第3张图片

示例图片:
周杰伦用户:显示周杰伦点赞过的评论变色
vue+elementUI+后端springboot多用户评论、回复、点赞_第4张图片

刘德华用户:显示刘德华点赞过的评论变色
vue+elementUI+后端springboot多用户评论、回复、点赞_第5张图片

前端页面vue
ComTest.vue

<template>
  <div class="comment">
    <div>
      <div v-clickoutside="hideReplyBtn" @click="inputFocus" class="my-reply">
        <el-avatar class="header-img" :size="40" :src="avatar">el-avatar>
        <div class="reply-info">
          <div
            tabindex="0"
            contenteditable="true"
            id="replyInput"
            spellcheck="false"
            placeholder="输入评论..."
            class="reply-input"
            @focus="showReplyBtn"
            @input="onDivInput($event)"
          >div>
        div>
        <div class="reply-btn-box" v-show="btnShow">
          <el-button
            class="reply-btn"
            size="medium"
            @click="sendComment"
            type="primary"
            >发表评论el-button
          >
        div>
      div>
      <div
        v-for="(item, i) in comments"
        :key="i"
        class="author-title reply-father"
      >
        <el-avatar class="header-img" :size="40" :src="item.avatar">el-avatar>
        <div class="author-info">
          <span class="author-name">{{ item.username }}span>
          <span class="author-time">{{ item.time }}span>
        div>
        <div class="icon-btn">
          <span @click="showReplyInput(i, item.username, item.id)">
            <i class="iconfont el-icon-s-comment">i>{{ item.commentNum }}
          span>
          <i
            v-if="item.likeFlag"
            class="iconfont el-icon-caret-top likeIcon"
            @click="like(item.id)"
          >i>
          <i
            v-else
            class="iconfont el-icon-caret-top"
            @click="like(item.id)"
          >i
          >{{ item.like }}
        div>
        <div class="talk-box">
          <p>
            <span class="reply"> {{ item.comment }}span>
          p>
        div>
        <div class="reply-box">
          <div v-for="(reply, j) in item.reply" :key="j" class="author-title">
            <el-avatar
              class="header-img"
              :size="40"
              :src="reply.avatar"
            >el-avatar>
            <div class="author-info">
              <span class="author-name">{{ reply.username }}span>
              <span class="author-time">{{ reply.time }}span>
            div>
            <div class="icon-btn">
              <span @click="showReplyInput(i, reply.username, reply.id)"
                ><i class="iconfont el-icon-s-comment">i
                >{{ reply.commentNum }}span
              >
              <i
                v-if="reply.likeFlag"
                class="iconfont el-icon-caret-top likeIcon"
                @click="like(reply.id)"
              >i>
              <i
                v-else
                class="iconfont el-icon-caret-top"
                @click="like(reply.id)"
              >i
              >{{ reply.like }}
            div>
            <div class="talk-box">
              <p>
                回复<span> @{{ reply.parentName }}: span>
                <span class="reply"> {{ reply.comment }}span>
              p>
            div>
            <div class="reply-box">div>
          div>
        div>
        <div v-show="_inputShow(i)" class="my-reply my-comment-reply">
          <el-avatar class="header-img" :size="40" :src="avatar">el-avatar>
          <div class="reply-info">
            <div
              tabindex="0"
              contenteditable="true"
              spellcheck="false"
              :placeholder="placeholder"
              @input="onDivInput($event)"
              class="reply-input reply-comment-input"
            >div>
          div>
          <div class="reply-btn-box">
            <el-button
              class="reply-btn"
              size="medium"
              @click="sendCommentReply(i)"
              type="primary"
              >发表评论el-button
            >
          div>
        div>
      div>
    div>
  div>
template>

<script>
import axios from "axios";
const clickoutside = {
  // 初始化指令
  bind(el, binding, vnode) {
    function documentHandler(e) {
      // 这里判断点击的元素是否是本身,是本身,则返回
      if (el.contains(e.target)) {
        return false;
      }
      // 判断指令中是否绑定了函数
      if (binding.expression) {
        // 如果绑定了函数 则调用那个函数,此处binding.value就是handleClose方法
        binding.value(e);
      }
    }
    // 给当前元素绑定个私有变量,方便在unbind中可以解除事件监听
    el.vueClickOutside = documentHandler;
    document.addEventListener("click", documentHandler);
  },
  update() {},
  unbind(el, binding) {
    // 解除事件监听
    document.removeEventListener("click", el.vueClickOutside);
    delete el.vueClickOutside;
  },
};
export default {
  name: "ComTest", // 组件名称
  data() {
    return {
      placeholder: "", // 回复者名称
      btnShow: false,
      index: "0",
      replyComment: "", // 评论输入内容
      username: "jaychou", // 登录的用户名
      avatar: "https://s1.ax1x.com/2022/06/10/Xc9lUf.png", // 登录用户头像
      userId: "", // 登录用户id
      parentName: "", // 回复的对象(父评论)用户名
      parentId: 0, // 父id
      itemId: "6666", // 文章等等id
      comments: [
        {
          // username:'Lana Del Rey',
          // id:19870621,
          // avatar:'https://ae01.alicdn.com/kf/Hdd856ae4c81545d2b51fa0c209f7aa28Z.jpg',
          // parentName:'', // 父评论名
          // parentId:'', // 父评论id
          // comment:'我发布一张新专辑Norman Fucking Rockwell,大家快来听啊',
          // time:'2019年9月16日 18:43',
          // commentNum:2, // 该评论的回复条数
          // like:15, // 点赞
          // likeFlag=true, // 点赞图标状态颜色变化
          // inputShow:false, // 输入框隐藏
          // reply:[
          //     {
          //         username:'Taylor Swift',
          //         id:19891221,
          //         avatar:'https://ae01.alicdn.com/kf/H94c78935ffa64e7e977544d19ecebf06L.jpg',
          //         parentName:'Lana Del Rey',
          //         parentId:19870621,
          //         comment:'我很喜欢你的新专辑!!',
          //         time:'2019年9月16日 18:43',
          //         commentNum:1,
          //         like:15,
          //         likeFlag=true, // 点赞图标状态颜色变化
          //         inputShow:false
          //     }
          //]
        },
      ],
    };
  },
  directives: { clickoutside },
  created() {
    // 注意this
    // 获取用户登录信息
    this.getLoginUser();
  },
  methods: {
    myrefresh() {
      //console.log(this.userId)
     // alert(this.userId+"nmnm")
      //let url = `/api1/ts/tcomment/commentList/${this.itemId}/${this.userId}`
      //查询评论信息列表展示, 文章id/用户id 
      axios.get("http://localhost:8080/ts/tcomment/commentList/"+this.itemId+"/"+this.userId).then((resp) => {
          if (resp.data.success) {
            let list = resp.data.data;
            console.log(list.comments);
            this.comments = list.comments;
          }
        });
    },
    getLoginUser() {
      // 后台session获取登录信息
      axios.get("http://localhost:8080/ts/tuser/getLoginUser").then((resp) => {
        if (resp.data.success) {
          let user = resp.data.data.user;
          console.log(user);
          if (user == null) {
            this.avatar = "https://s1.ax1x.com/2022/06/10/Xc9lUf.png"; // 没登陆时,默认头像地址
          } else {
            this.username = user.username;
            this.userId = user.id;
            console.log(this.userId)
            //alert(this.userId)
            this.avatar = user.avatar;
          }
        }
      }).finally(()=>{
        this.myrefresh() // 刷新
      });
    },
    inputFocus() {
      var replyInput = document.getElementById("replyInput");
      replyInput.style.padding = "8px 8px";
      replyInput.style.border = "2px solid #409EFF";
      replyInput.focus();
    },
    showReplyBtn() {
      this.btnShow = true;
    },
    hideReplyBtn() {
      this.btnShow = false;
      replyInput.style.padding = "10px";
      replyInput.style.border = "none";
    },
    showReplyInput(i, name, id) {
      this.comments[this.index].inputShow = false;
      this.index = i;
      this.comments[i].inputShow = true;
      this.parentName = name;
      this.parentId = id;
      this.placeholder = "回复 @" + name;
      //alert(i)
    },
    _inputShow(i) {
      return this.comments[i].inputShow;
    },
    sendComment() {
      // 父评论
      if (!this.replyComment) {
        this.$message({
          showClose: true,
          type: "warning",
          message: "评论不能为空",
        });
      } else {
        let a = {};

        a.userId = this.userId;
        a.username = this.username;
        a.content = this.replyComment;
        a.avatar = this.avatar;
        a.itemId = 6666;
        a.parentId = "0";
        a.parentName = "";

        axios
          .post("http://localhost:8080/ts/tcomment/addComment", a)
          .then((response) => {
            if (response.data.success) {
              console.log(response.data);
              this.$message.success("评论成功!");
            } else {
              this.$message.error("评论失败,请稍后重试!");
            }
          })
          .finally(() => {
            this.myrefresh();
          });
        //
        document.getElementById("replyInput").innerHTML = "";
        this.replyComment = "";
      }
    },
    sendCommentReply(i) {
      // 子回复提交
      if (!this.replyComment) {
        this.$message({
          showClose: true,
          type: "warning",
          message: "评论不能为空",
        });
      } else {
        // 组装请求数据
        let a = {};

        a.userId = this.userId;
        a.username = this.username;
        a.content = this.replyComment;
        a.avatar = this.avatar;
        a.itemId = 6666;
        a.parentId = this.parentId;
        a.parentName = this.parentName;
        //
        axios
          .post("http://localhost:8080/ts/tcomment/addComment", a)
          .then((response) => {
            if (response.data.success) {
              this.$message.success("回复成功!");
            } else {
              this.$message.error("回复失败,请稍后重试!");
            }
          })
          .finally(() => {
            this.myrefresh();
          });

        this.replyComment = "";
        document.getElementsByClassName("reply-comment-input")[i].innerHTML =
          "";
      }
    },
    onDivInput: function (e) {
      this.replyComment = e.target.innerHTML;
    },
    like(id) {
      //点赞  评论id
      // alert(id)
      //组装数据
      let a = {
        commentId: id,
        userId: this.userId,
      };
      axios
        .post("http://localhost:8080/ts/tlike/likeControl", a)
        .then((resp) => {
          //this.$message.success("成功!!")
          this.myrefresh();
        });
    },
  },
};
script>

<style>
.comment {
  width: 1000px;
  margin: 0 auto;
  font-family: PingFang SC, HarmonyOS_Regular, Helvetica Neue, Microsoft YaHei,
    sans-serif;
}
.my-reply {
  padding: 10px;
  background-color: #fafbfc;
}
.my-reply .header-img {
  display: inline-block;
  vertical-align: top;
}
.my-reply .reply-info {
  display: inline-block;
  margin-left: 5px;
  width: 90%;
}
@media screen and (max-width: 1200px) {
  .my-reply .reply-info {
    width: 80%;
  }
}
.my-reply .reply-info .reply-input {
  min-height: 20px;
  line-height: 22px;
  padding: 10px 10px;
  color: #ccc;
  background-color: #fff;
  border-radius: 5px;
}
.my-reply .reply-info .reply-input:empty:before {
  content: attr(placeholder);
}
.my-reply .reply-info .reply-input:focus:before {
  content: none;
}
.my-reply .reply-info .reply-input:focus {
  padding: 8px 8px;
  border: 2px solid #409eff;
  box-shadow: none;
  outline: none;
}
/* .reply-info>div .reply-input:focus{
    border: 2px solid #409EFF;
} */
.my-reply .reply-btn-box {
  height: 25px;
  margin: 10px 0;
}
.my-reply .reply-btn-box .reply-btn {
  position: relative;
  float: right;
  margin-right: 15px;
}
.my-comment-reply {
  margin-left: 50px;
}
.my-comment-reply .reply-input {
  width: flex;
}
.author-title:not(:last-child) {
  border-bottom: 1px solid rgba(178, 186, 194, 0.3);
}
.author-title {
  padding: 10px;
}
.author-title .header-img {
  display: inline-block;
  vertical-align: top;
}
.author-title .author-info {
  display: inline-block;
  margin-left: 5px;
  width: 60%;
  height: 40px;
  line-height: 20px;
}
.author-title .author-info > span {
  display: block;
  cursor: pointer;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.author-title .author-info .author-name {
  color: #303133;
  font-size: 18px;
  font-weight: 500;
}
.reply-box .talk-box {
  color: #606266;
}
.reply-box .talk-box span {
  color: #6298ce;
}
.author-title .author-info .author-time {
  font-size: 14px;
}
.author-time {
  color: #606266;
}
.author-title .icon-btn {
  width: 30%;
  padding: 0 !important;
  float: right;
}
@media screen and (max-width: 1200px) {
  .author-title .icon-btn {
    width: 20%;
    padding: 7px;
  }
}
.author-title .icon-btn > span {
  cursor: pointer;
}
.author-title .icon-btn .iconfont {
  margin: 0 5px;
}
.author-title .talk-box {
  margin: 0 50px;
}
.author-title .talk-box > p {
  margin: 0;
}
.author-title .talk-box .reply {
  font-size: 16px;
  color: #606266;
}

.author-title .reply-box {
  margin: 10px 0 0 50px;
  background-color: #efefef;
}
/* 点赞图标颜色 */
.likeIcon {
  color: #40a0ff;
}
style>

数据库

评论表:
vue+elementUI+后端springboot多用户评论、回复、点赞_第6张图片

用户表:
vue+elementUI+后端springboot多用户评论、回复、点赞_第7张图片

点赞表:
vue+elementUI+后端springboot多用户评论、回复、点赞_第8张图片

后端

vue+elementUI+后端springboot多用户评论、回复、点赞_第9张图片核心业务是 取出所有评论 并递归组装为只有二层评论:顶级 >子级(包含子子孙孙级),还有就是点赞业务统计点赞数并判断该登录用户是否对评论点赞,点赞要将图标变色,同一个用户点一次+1,再点-1。

部分代码:TCommentVOServiceImpl

/**
 * 

* 评论服务实现类 *

* * @author panqiyi * @since 2022-06-05 */
@Service public class TCommentVOServiceImpl extends ServiceImpl<TCommentMapper, TComment> implements TCommentService { // 保存评论 @Override public Boolean saveComment(TComment TComment) { boolean save = save(TComment); return save; } // 返回 评论列表 @Override public List<CommentVo> getCommentList(String itemId) { // 1、获取id=itemId文章下的 所有评论 QueryWrapper<TComment> allwrapper = new QueryWrapper<>(); allwrapper.eq("item_id",itemId); List<TComment> allList = list(allwrapper); // 所有评论集合 // 遍历所有评论集合,复制到 评论视图返回类 ArrayList<CommentVo> commentVos = new ArrayList<>(); //评论视图返回类 集合 for (TComment tComment : allList) { CommentVo commentVo = new CommentVo(); BeanUtils.copyProperties(tComment,commentVo); commentVo.setTime(tComment.getTCreate()); // 时间 commentVo.setComment(tComment.getContent()); // 内容 commentVos.add(commentVo); } // for (CommentVo commentVo : commentVos) { // System.out.println(commentVo); // } // // System.out.println(".................."); ArrayList<CommentVo> com = new ArrayList<>(); // 最终集合 HashMap<String, CommentVo> map = new HashMap<>(); // 2、com集合存储顶级评论 for (CommentVo tComment : commentVos) { // 遍历每一条评论 if ("0".equals(tComment.getParentId())){ // 顶级评论 com.add(tComment); } map.put(tComment.getId(),tComment); // 所有评论 id:评论对象 } // 3、com集合内顶级评论 存储所有包含的子级评论 for (CommentVo tComment : commentVos) { // 遍历每一条评论 if (!"0".equals(tComment.getParentId())){ // 非顶级评论,既第一子级、第二子级等等评论 // 获取所有父级评论 (如 a->b, b->c, c->d, d="0") CommentVo parent = map.get(tComment.getParentId()); if (parent == null){ continue; // 跳出本次循环 } if (parent.getId().equals(tComment.getId())){ continue; } parent.getReply().add(tComment); // 所有父级存储子级 (b(reply[b]), c(replay[b(reply[a])]) ....) } } /*System.out.println("================="); for (CommentVo tComment : com) { System.out.println(tComment); }*/ // 结果是 顶级包含子级,子级包含孙级....,层次嵌套, // 但是我们想返回的是 父级>子级(包含所有子子孙孙) 两层 // 组装好的评论数据 List<CommentVo> formatList = getFormat(com); return formatList; } // 存储 新组装好的 子级(子子孙孙) 数据集合 ArrayList<CommentVo> newReplys = new ArrayList<>(); /** * 重新组装评论集合,使其只有两层 顶级评论>第一子级评论(包含子级和子子孙孙) * @param com * @return */ public List<CommentVo> getFormat(List<CommentVo> com){ for (CommentVo tComment : com) { // 遍历顶点集合 // 获取顶点 子级集合 List<CommentVo> replys = tComment.getReply(); for (CommentVo comment : replys) { // 遍历第一级(子级)回复集合 ( for(:) 只有集合元素>0才会进入) newReplys.add(comment); // 存储第一级子评论 recursively(comment); // 递归子级评论 } tComment.setReply(newReplys); // 重新设置子级(包含子子孙孙) tComment.setCommentNum(newReplys.size()); // 低级评论回复数 // 新建(或清除)组装集合,避免下一轮顶点集合组装有数据 newReplys = new ArrayList<>(); } return com; } /** * 递归子级评论,返回顶级评论下 所有子代 都放到第一子级评论中 * @param comment */ private void recursively(CommentVo comment){ List<CommentVo> replys = comment.getReply(); // 第二子级集合 if (replys.size()>0){ // 存在评论 for (CommentVo reply : replys) { newReplys.add(reply); // 存储 if (reply.getReply().size()>0){ recursively(reply); // 递归 } } } } }

建议直接上面我git链接弄下来看完整的。

你可能感兴趣的:(SpringBoot,前端,vue.js,elementui,spring,boot)