Laravel+Vue 構建仿簡書二級評論系統包含郵件通知

默之藍發表於2019-11-23

寫在前面

這篇文章嚴格來講是將已有的仿簡書二級評論系統和 Laravel、Vue 進行結合並改進,例如新增郵件通知。前人栽樹後人乘涼,評論系統的資料結構和 Vue 模板詳情見下面這篇文章,本文不在贅述。

  1. vue + element-ui + scss 仿簡書評論模組

專案原始碼

前端評論模板https://gitee.com/bluish_space/lvblog/blob...

前端 Vuex 模組
https://gitee.com/bluish_space/lvblog/blob...

後端資料處理https://gitee.com/bluish_space/lvblog/blob...

後端資料轉換器
https://gitee.com/bluish_space/lvblog/blob...

前端模板

<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);
        }
    }

郵件通知

觀察者

評論的觀察者
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);
    }

這樣我們在收到評論或回覆時就能夠及時收到郵件通知啦!
在這裡插入圖片描述

更多資訊

關注下我的微信公眾號就是對我創作的最大支援~

相關文章