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

该项目展示了如何使用Vue、ElementUI前端框架和SpringBoot后端实现评论、回复和点赞功能。前端利用vue-VLI脚手架、axios进行数据交互,后端使用SpringBoot、Mybatis-Plus。数据库包括评论表、用户表和点赞表,支持评论层级展示和点赞状态判断。
摘要由CSDN通过智能技术生成

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

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

演示

评论:
在这里插入图片描述

回复:
在这里插入图片描述

点赞、取消点赞(图标颜色变化):
在这里插入图片描述

示例图片:
周杰伦用户:显示周杰伦点赞过的评论变色
在这里插入图片描述

刘德华用户:显示刘德华点赞过的评论变色
在这里插入图片描述

前端页面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>

数据库

评论表:
在这里插入图片描述

用户表:
在这里插入图片描述

点赞表:
在这里插入图片描述

后端

在这里插入图片描述核心业务是 取出所有评论 并递归组装为只有二层评论:顶级 >子级(包含子子孙孙级),还有就是点赞业务统计点赞数并判断该登录用户是否对评论点赞,点赞要将图标变色,同一个用户点一次+1,再点-1。

部分代码:TCommentVOServiceImpl

/**
 * <p>
 *  评论服务实现类
 * </p>
 *
 * @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链接弄下来看完整的。

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值