Vue+elementUI实现评论功能


参考链接 https://blog.csdn.net/zLanaDelRey/article/details/100997792

前端代码主要参考以上链接,效果类似,后端代码自己封装,构建评论数据

前端代码  

可作为组件直接使用,需要传入knowlgIdParam文章id使用

<template>
    <div>
        <div v-clickoutside="hideReplyBtn" @click="inputFocus" class="my-reply">
            <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">
            <div class="author-info">
                <span class="author-commentUser">{{item.commentUser}}span>
                <span class="author-time">{{item.updatedDate}}span>
            div>
            <div class="icon-btn">
                <span class="reply-span" @click="showReplyInput(i,item.commentUser,item.commentId)">
                    回复
                span>
            div>
            <div class="talk-box">
                <p>
                    <span class="reply">{{item.commentContent}}span>
                p>
            div>
            <div class="reply-box">
                <div v-for="(reply,j) in item.reply" :key="j" class="author-title">
                    <div class="author-info">
                        <span class="author-commentUser">{{reply.commentUser}}span>
                        <span class="author-time">{{reply.updatedDate}}span>
                    div>
                    <div class="icon-btn">
                        <span class="reply-span" @click="showReplyInput(i,reply.commentUser,reply.commentId)">
                            回复span>
                    div>
                    <div class="talk-box">
                        <p>
                            <span class="reply"><span class="reply-span-down">回复span> @{{reply.toCommentUser}}:span>
                            <span class="reply">{{reply.commentContent}}span>
                        p>
                    div>
                    <div class="reply-box">

                    div>
                div>
            div>
            <div v-show="_inputShow(i)" class="my-reply my-comment-reply">
                <div class="reply-info">
                    <div tabindex="0" contenteditable="true" spellcheck="false" 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,j)" type="primary">发表评论
                    el-button>
                div>
            div>
        div>
    div>
template>

<script>
    import {axios} from 'utils/';
    import {cbs, gbs} from 'config/';

    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: 'ArticleComment',
        data() {
            return {
                btnShow: false,
                index: '0',
                replyComment: '',
                myName: 'Lana Del Rey',
                toCommentUser: '',
                parentCommentId: -1,
                comments: [
                    {
                        commentUser: '',
                        commentId: '',
                        commentContent: '',
                        updatedDate: '',
                        inputShow: false,
                        reply: [
                            {
                                commentUser: '',
                                commentId: '',
                                toCommentUser: '',
                                parentCommentId: '',
                                commentContent: '',
                                updatedDate: '',
                                inputShow: false
                            }
                        ]
                    }
                ]
            }
        },
        props: {
            // 接收父组件传值的变量
            knowlgIdParam: {
                type: String,
                default: () => {
                    return null
                }
            }
        },
        directives: {clickoutside},
        created() {
            // this.$route.query.knowlgId
            this.getCurrentUser()
            this.getCommentData()
        },
        methods: {
            getCurrentUser(){
                axios({
                    type: 'get',
                    path: gbs.adminContext + '/knowlgComment/getCurrentUser',
                    fn: res => {
                        this.myName = res.data
                        console.log(res.data,'res.data')
                    },
                    errFn: res => {
                        if (res.data != undefined) {
                            that.$message.error('请求出错:' + res)
                        }
                    }
                });
            },
            getCommentData() {
                axios({
                    type: 'get',
                    path: gbs.adminContext + '/knowlgComment/listMain',
                    data: {knowlgId: this.knowlgIdParam},
                    fn: res => {
                        this.comments = res.data
                        console.log(res.data,'res.data')
                        console.log(res.data.reply,'res.data.reply')
                    },
                    errFn: res => {
                        if (res.data != undefined) {
                            that.$message.error('请求出错:' + res)
                        }
                    }
                });
            },
            saveComment(comment){
                axios({
                    type: 'post',
                    path: gbs.adminContext + '/knowlgComment/opeMain',
                    data: comment,
                    headers: {'Content-Type': 'application/json;charset=UTF-8'},
                    fn: res => {
                    },
                    errFn: res => {
                        if (res.data != undefined) {
                            this.$message.error('请求出错:' + res)
                        }
                    }
                });
            },
            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, commentUser, commentId) {
                this.comments[this.index].inputShow = false
                this.index = i
                this.comments[i].inputShow = true
                this.toCommentUser = commentUser
                this.parentCommentId = commentId
            },
            _inputShow(i) {
                return this.comments[i].inputShow
            },
            sendComment() {
                if (!this.replyComment) {
                    this.$message({
                        showClose: true,
                        type: 'warning',
                        message: '评论不能为空'
                    })
                } else {
                    let a = {}
                    let input = document.getElementById('replyInput')
                    a.commentUser = this.myName
                    a.commentContent = this.replyComment
                    a.knowlgId = this.knowlgIdParam
                    this.comments.push(a)
                    this.replyComment = ''
                    input.innerHTML = ''
                    this.saveComment(a)
                }
            },
            sendCommentReply(i, j) {
                if (!this.replyComment) {
                    this.$message({
                        showClose: true,
                        type: 'warning',
                        message: '评论不能为空'
                    })
                } else {
                    let a = {}
                    a.commentUser = this.myName
                    a.toCommentUser = this.toCommentUser
                    a.commentContent = this.replyComment
                    a.parentCommentId = this.parentCommentId
                    a.knowlgId = this.knowlgIdParam
                    if(!this.comments[i].reply){
                        this.comments[i].reply = []
                    }
                    this.comments[i].reply.push(a)
                    this.replyComment = ''
                    document.getElementsByClassName("reply-comment-input")[i].innerHTML = ""
                    this.saveComment(a)
                }
            },
            onDivInput: function (e) {
                this.replyComment = e.target.innerHTML;
            },
        },
    }
script>

<style lang="stylus" scoped>
    .my-reply
        padding 10px
        background-color #fafbfc

        .header-img
            display inline-block
            vertical-align top

        .reply-info
            display inline-block
            margin-left 5px
            width 90%
            @media screen and (max-width: 1200px) {
                width 80%
            }

            .reply-input
                min-height 20px
                line-height 22px
                padding 10px 10px
                color #ccc
                background-color #fff
                border-radius 5px

                &:empty:before
                    content attr(placeholder)

                &:focus:before
                    content none

                &:focus
                    padding 8px 8px
                    border 2px solid #409EFF
                    box-shadow none
                    outline none

        .reply-btn-box
            height 25px
            margin 10px 0

            .reply-btn
                position relative
                float right
                margin-right 15px

    .my-comment-reply
        margin-left 50px

        .reply-input
            width flex

    .reply-span
        font-size 14px
        color #909399

    .reply-span-down
        font-size 12px
        color #909399

    .author-title:not(:last-child)
        border-bottom: 1px solid rgba(178, 186, 194, .3)

    .author-title
        padding 10px

        .header-img
            display inline-block
            vertical-align top

        .author-info
            display inline-block
            margin-left 5px
            width 60%
            height 40px
            line-height 20px

            > span
                display block
                cursor pointer
                overflow hidden
                white-space nowrap
                text-overflow ellipsis

            .author-commentUser
                font-size 14px
                font-weight bold

            .author-time
                font-size 12px
                color #909399

        .icon-btn
            width 30%
            padding 0 !important
            float right
            @media screen and (max-width: 1200px) {
                width 20%
                padding 7px
            }

            > span
                cursor pointer

            .iconfont
                margin 0 5px

        .talk-box
            margin 0 50px

            > p
                margin 0

            .reply
                font-size 14px

        .reply-box
            margin 10px 0 0 50px
style>

后端主要代码

实体类

构建评论数据方法

/**
     * @description 构建两层评论数据
     * @param [item]
     * @return java.util.List
     **/
    @Override
    public List buildCommentTree(KnowlgCommentDomain item){
        List reList = new ArrayList<>();
        // 获取树查询需要的commentIds
        List listCI = selectCommentIds(item);
        // 根据knId获取列表,用于获取 评论被回复人
        List listAll = itemDao.selectListByKnId(item);
        // 循环开始构建评论树,只需要两层
        for(KnowlgCommentDomain k:listCI){
            List reply = new ArrayList<>();
            // 根据commentId进行递归查询 即单独获取一条需要组装的评论数据
            List list = selectListByTree(k);

            // 将list转换为一条评论数据 null是顶层评论Id,其余为reply
            KnowlgCommentVO v = new KnowlgCommentVO();
            for(KnowlgCommentDomain c:list){
                if("null".equals(c.getParentCommentId())){
                    BeanUtils.copyProperties(c,v);
                    v.setInputShow(false);
                    v.setToCommentUser(getToCommentUser(c,listAll));
                    break;
                }
            }
            for(KnowlgCommentDomain c:list){
                if(!("null".equals(c.getParentCommentId()))){
                    KnowlgCommentVO kc = new KnowlgCommentVO();
                    BeanUtils.copyProperties(c,kc);
                    kc.setInputShow(false);
                    kc.setToCommentUser(getToCommentUser(c,listAll));
                    reply.add(kc);
                }
            }
            v.setReply(reply);
            reList.add(v);
        }
        return reList;
    }/**
* @description 根据Id获取评论被回复人
     * @param [comment, list]
     * @return java.lang.String
     **/
    private String getToCommentUser(KnowlgCommentDomain comment,List list){
        forif(comment.getParentCommentId().equals(k.getCommentId())){
                return k.getCommentUser();
            }
        }
        return null;
   

主要用到的sql

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sinolife.knowledge.manage.dao.KnowlgCommentDao">
    
    <resultMap type="com.sinolife.api.domain.manage.KnowlgCommentDomain" id="KnowlgCommentResult">
        <result property="commentId"  column="COMMENT_ID" jdbcType="VARCHAR"  />
        <result property="knowlgId"  column="KNOWLG_ID" jdbcType="VARCHAR"  />
        <result property="commentUser"  column="COMMENT_USER" jdbcType="VARCHAR"  />
        <result property="commentContent"  column="COMMENT_CONTENT" jdbcType="VARCHAR"  />
        <result property="parentCommentId"  column="PARENT_COMMENT_ID" jdbcType="VARCHAR"  />
        <result property="isValid"  column="IS_VALID" jdbcType="VARCHAR"  />
    resultMap>
    
     <select id="selectListByTree" parameterType="com.sinolife.api.domain.manage.KnowlgCommentDomain" 
resultMap="KnowlgCommentResult"> select * from ( select <include refid="selectKnowlgCommentVo"/> from KNOWLG_COMMENT START WITH COMMENT_ID = #{commentId} CONNECT BY PARENT_COMMENT_ID = PRIOR COMMENT_ID) <where> 1=1 and IS_VALID = 'Y' where> <if test="sortField != null and sortField != '' and direction != null and direction !=''"> order by ${sortField} ${direction} if> select> <select id="selectCommentIds" parameterType="com.sinolife.api.domain.manage.KnowlgCommentDomain"
resultMap="KnowlgCommentResult"> select COMMENT_ID from KNOWLG_COMMENT <where> 1=1 and KNOWLG_ID = #{knowlgId} and PARENT_COMMENT_ID = 'null' and IS_VALID = 'Y' where> <if test="sortField != null and sortField != '' and direction != null and direction !=''"> order by ${sortField} ${direction} if> select> mapper>