在这里插入图片描述

写在前面

这篇文章严格来讲是将已有的仿简书二级评论系统和 Laravel、Vue 进行结合并改进,例如添加邮件通知。前人栽树后人乘凉,评论系统的数据结构和 Vue 模板详情见下面这篇文章,本文不在赘述。

  1. vue + element-ui + scss 仿简书评论模块

项目源码

前端评论模板https://gitee.com/bluish_space/lvblog/blob/master/resources/js/components/Art/comment.vue

前端 Vuex 模块
https://gitee.com/bluish_space/lvblog/blob/master/resources/js/modules/comments.js

后端数据处理https://gitee.com/bluish_space/lvblog/blob/master/app/Http/Controllers/API/CommentController.php

后端数据转换器
https://gitee.com/bluish_space/lvblog/blob/master/app/Transformers/CommentTransformer.php

前端模板

<script>
    //登录的事件总线
    import {EventBus} from '../../event-bus.js';
    export default {
        name: "comment",
        data() {
            return {
                loader:'',
                tar:'',
                inputComment: '',
                inputReply:'',
                showItemId: '',

                //发布评论
                //发布评论占用信号量,PV操作,单线程
                comment_buss: 1,
                isReply: 0,
                idReply: 0,
                idComment:0,

                //删除评论
                //删除评论占用信号量,PV操作
                delete_buss: 1,
                interval:'',
                jumped:0,
            }
        },
        created(){
            //从 Vuex 获取评论的数据
            this.$store.dispatch('loadComments',{
                art_id: this.$route.params.art_id,
            });
            //监听评论数据的加载情况
            this.$watch(this.$store.getters.getCommentsLoadStatus, function () {
                if (this.$store.getters.getCommentsLoadStatus() == 3) {
                    console.log('comment.vue:评论模块未能成功加载!')
                }
            });
        },
        mounted(){
            //邮件通知时用于跳转到指定评论或回复的锚点的方法
            this.anchor();
        },
        watch: {
            // 如果路由有变化,会再次执行该 stopInterval 方法
               //如果已经跳转到相应锚点,清楚定时执行操作
            "jumped": "stopInterval"
        },
        computed:{
            //评论的计算属性
            comments(){
                return this.$store.getters.getComments.data;
            },
               //用户的计算属性,判断是否有用户登录
            user(){
                return this.$store.getters.getUser;
            },
               //当前评论所属文章的计算属性
            article(){
                return this.$store.getters.getArticle.data;
            }
        },
        methods: {
            //清楚定时执行器
            stopInterval(){
                window.clearInterval(this.interval);
            },
            anchor(){
                if(this.$route.query.reply !== undefined){
                    //判断是评论还是回复
                    let type = this.$route.query.reply;
                    //定位目标评论或回复的位置
                    let location = this.$route.query.location;
                    //拼接锚点
                    let anchor = '#'+type+location;
                    let jump = '';
                    this.$nextTick(()=> {
                        this.interval = setInterval(()=> {
                            jump = document.querySelectorAll(anchor);
                            if(jump.length!=0) {
                                // 滚动到目标位置
                                document.querySelector(anchor).scrollIntoView(true);
                                this.jumped ++;
                            }
                        })
                    },500);
                }
            },
            /**
             * 点赞
             */
            likeClick(item) {
                if(!this.$store.getters.getUser){
                    this.login();
                }
                if (!item.isLike) {
                    this.$store.dispatch('likeComment',{
                        comment_id:item.id
                    });
                    this.$watch(this.$store.getters.getCommentLikeStatus, function () {
                        if (this.$store.getters.getCommentLikeStatus() == 2) {
                            this.$set(item, "isLike", true);
                            item.likeNum++;
                        }
                        if (this.$store.getters.getCommentLikeStatus() == 3) {
                            this.$message.warning('点赞失败了,请稍后重试!');
                        }
                    });
                } else {
                    this.$message.info('你已经赞过了哦~');
                }
            },

            /**
             * 点击取消按钮
             */
            cancel() {
                this.showItemId = '';
                this.inputComment = this.inputReply = '';
                ++this.comment_buss;
            },

            /**
             * 提交评论
             */
            commitComment() {
                if(!this.$store.getters.getUser){
                    this.login();
                    return false;
                }
                --this.comment_buss;
                if(this.comment_buss<0){
                    this.message.warning('有其他进程在执行评论操作,请稍候重试!')
                }else{
                    if(this.isReply == 1){
                        if(this.inputReply == ''){
                            this.$message.warning('回复内容不能为空');
                            return false;
                        }
                        this.$store.dispatch('postReply',{
                            comment_id:this.idComment,
                            contents:this.inputReply,
                            toUser : this.idReply,
                            art_id : this.$route.params.art_id,
                        });
                        this.loader = this.$loading({
                            lock: true,
                            text: '发布回复中...',
                            spinner: 'el-icon-loading',
                            background: 'rgba(0, 0, 0, 0.7)'
                        });
                        this.$watch(this.$store.getters.getReplyPostStatus, function () {
                            if (this.$store.getters.getReplyPostStatus() == 2) {
                                this.loader.close();
                                this.isReply = 0;
                                this.idReply = 0;
                                this.cancel();
                                this.$message.success('回复成功!');
                            }
                            if (this.$store.getters.getReplyPostStatus() == 3) {
                                this.loader.close();
                                this.$message.warning('回复失败了,请稍后重试!');
                            }
                        });

                    }else{
                        if(this.inputComment == ''){
                            this.$message.warning('评论内容不能为空');
                            return false;
                        }
                        this.$store.dispatch('postComment',{
                            art_id:this.$route.params.art_id,
                            contents:this.inputComment
                        });
                        this.loader = this.$loading({
                            lock: true,
                            text: '发布评论中...',
                            spinner: 'el-icon-loading',
                            background: 'rgba(0, 0, 0, 0.7)'
                        });
                        this.$watch(this.$store.getters.getCommentPostStatus, function () {
                            if (this.$store.getters.getCommentPostStatus() == 2) {
                                this.loader.close();
                                this.$message.success('评论成功!');
                            }
                            if (this.$store.getters.getCommentPostStatus() == 3) {
                                this.loader.close();
                                this.$message.warning('评论失败了,请稍后重试!');
                            }
                        });
                    }
                    ++this.comment_buss;
                }

            },
            deleteComment(item, reply){
                if(!this.$store.getters.getUser){
                    this.login();
                    return false;
                }
                --this.delete_buss;
                if(this.delete_buss < 0){
                    this.$message.error('有其他进程在执行删除操作,请稍后重试!')
                }else{
                    if (reply) {
                        this.$confirm('此操作将永久删除此回复, 是否继续?', '提示', {
                            confirmButtonText: '确定',
                            cancelButtonText: '取消',
                            type: 'warning'
                        }).then(() => {
                            this.$store.dispatch('deleteReply', {
                                reply_id: reply.id,
                                art_id : this.$route.params.art_id,
                            });
                            this.loader = this.$loading({
                                lock: true,
                                text: '删除中...',
                                spinner: 'el-icon-loading',
                                background: 'rgba(0, 0, 0, 0.7)'
                            });
                            this.$watch(this.$store.getters.getReplyDeleteStatus, function () {
                                if (this.$store.getters.getReplyDeleteStatus() == 2) {
                                    this.loader.close();
                                    this.$message.success('已删除');
                                }
                                if (this.$store.getters.getReplyDeleteStatus() == 3) {
                                    this.loader.close();
                                    this.$message.error('删除回复失败了!')
                                }
                            });
                        }).catch(() => {
                            this.$message({
                                type: 'info',
                                message: '已取消删除'
                            });
                        });

                    } else {
                        this.$confirm('此操作将永久删除此评论, 是否继续?', '提示', {
                            confirmButtonText: '确定',
                            cancelButtonText: '取消',
                            type: 'warning'
                        }).then(() => {
                            this.$store.dispatch('deleteComment',{
                                comment_id:item.id,
                                art_id : this.$route.params.art_id,
                            });
                            this.loader = this.$loading({
                                lock: true,
                                text: '删除中...',
                                spinner: 'el-icon-loading',
                                background: 'rgba(0, 0, 0, 0.7)'
                            });
                            this.$watch(this.$store.getters.getCommentDeleteStatus,function () {
                                if(this.$store.getters.getCommentDeleteStatus() == 2){
                                    this.loader.close();
                                    this.$message.success('已删除');
                                }
                                if(this.$store.getters.getCommentDeleteStatus() == 3){
                                    this.loader.close();
                                    this.$message.error('删除评论失败了!')
                                }
                            });
                        }).catch(() => {
                            this.$message({
                                type: 'info',
                                message: '已取消删除'
                            });
                        });
                    }
                    ++this.delete_buss;
                }
            },
            /**
             * 点击评论按钮显示输入框
             * item: 当前大评论
             * reply: 当前回复的评论
             */
            showCommentInput(item, reply) {
                if(!this.$store.getters.getUser){
                    this.login();
                    return false;
                }else{
                    this.idComment = item.id;
                    this.isReply = 1;
                    if (reply) {
                        this.idReply = reply.fromId;
                        this.inputReply = "@" + reply.fromName + " "
                    } else {
                        this.idReply = item.fromId;
                        this.inputReply = ''
                    }
                    this.showItemId = item.id
                }
            },
            login(){
                EventBus.$emit('prompt-login');
            },
            validateReply(reply){
                if (this.$store.getters.getUser){
                    return reply.fromId != this.$store.getters.getUser.id;
                }else{
                    return true;
                }
            }
        }
    }
</script>

实例:
在这里插入图片描述

后台处理

数据结构

评论表

Schema::create('comments', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->bigInteger('article_id')->unsigned()->index()->comment('文章id');
            $table->bigInteger('fromId')->unsigned()->index()->comment('评论者id');
            $table->integer('type')->comment('评论类型');
            $table->string('fromName')->comment('评论者昵称');
            $table->string('fromAvatar')->comment('评论者头像');
            $table->bigInteger('likeNum')->comment('点赞次数');
            $table->string('contents')->comment('评论内容');
            $table->timestamps();
        });

回复表

Schema::create('replies', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->bigInteger('comment_id')->index()->unsigned()->comment('评论id');
            $table->bigInteger('fromId')->index()->unsigned()->comment('评论者id');
            $table->string('fromName')->comment('评论者昵称');
            $table->string('fromAvatar')->comment('评论者头像');
            $table->bigInteger('toId')->comment('被评论者id');
            $table->string('toName')->comment('评论者昵称');
            $table->string('toAvatar')->comment('评论者昵称');
            $table->string('contents')->comment('评论内容');
            $table->timestamps();
        });

数据处理

以回复数据的处理为例,其它见源码

public function replyStore(ReplyRequest $request,$comment)
    {
        if ($comment = Comment::find($comment)){
            //通过 Authorization Token 确定当前登录的用户,等价于 Auth::guard('api')->user(),获取登录用户信息
            $fromUser = $this->user;
            $toUser = User::find($request->toUser);
            $data = new Reply();
            $data ->  comment_id      = $comment->id;
            $data ->  fromId          = $fromUser->id;
            $data ->  fromName        = $fromUser->name;
            $data ->  fromAvatar      = $fromUser->avatar;
            $data ->  toId            = $toUser->id;
            $data ->  toName          = $toUser->name;
            $data ->  toAvatar        = $toUser->avatar;
            // 使用 Str:after 方法去掉 '@' 符号 
            $data ->  contents    = Str::after(Str::after($request->contents,'@'.$toUser->name.' '),'@'.$toUser->name);
            if($data->save()) {
                return response()->json(['message' => '回复成功'], 201);
            }else{
                return response()->json(['message' => '回复失败'], 500);
            }
        }else{
            return response()->json(['message' => '目标评论不存在'], 404);
        }
    }

邮件通知

观察者

评论的观察者

#在评论数据插入数据库后执行下面的 created 方法:

public function created(Comment $comment)
    {
        //
        $article = $comment->article;
        $user =$article->user;
        // 如果要通知的人是当前用户,就不必通知了!
        if ($user ->id != auth('api')->user()->id) {
            $user->increment('notification_count');
            $user->notify(new ArticleReplied($comment));
        }
    }

回复的观察者

public function created(Reply $reply)
    {
        //
        // 如果要通知的人是当前用户,就不必通知了!
        if ($reply ->toId != auth('api')->user()->id) {
            $reply->toUser->increment('notification_count');
            $reply->toUser->notify(new CommentReplied($reply));
        }
    }

邮件通知

开启邮件通知频道,完成邮件通知方法

评论的邮件通知方法:

public function toMail($notifiable)
{
//        Log::debug($this);
    $url = env('APP_URL').'/art/' .  $this->comment->article_id . '?reply=comment&location=' . $this->comment->id;
    return (new MailMessage)
        ->line('你的文章有了新评论!')
        ->action('查看评论', $url);

}

回复的邮件通知方法

public function toMail($notifiable)
    {
        $url = env('APP_URL').'/art/' .  $this->reply->comment->article_id . '?reply=reply&location=' .  $this->reply->id;
        return (new MailMessage)
            ->line('你在文章下的评论有了新回复!')
            ->action('查看回复', $url);
    }

这样我们在收到评论或回复时就能够及时收到邮件通知啦!

在这里插入图片描述

更多信息


关注下我的微信公众号就是对我创作的最大支持~

在这里插入图片描述

一个极客迷