Laravel+Vue 构建支持邮件通知的二级评论系统

  • 2019 年 12 月 6 日
  • 筆記

写在前面

这篇文章严格来讲是将已有的仿简书二级评论系统和 Laravel、Vue 进行结合并改进,例如添加邮件通知,评论定位。前人栽树后人乘凉,评论系统的数据结构和 Vue 模板详情点击文章底部 阅读原文 查看。

评论模板内脚本核心代码解析

<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,                       }              },              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:评论模块未能成功加载!')                              }                              });                              },              computed:{                  //评论的计算属性                  comments(){                      return this.$store.getters.getComments.data;                      },                      //用户的计算属性,判断是否有用户登录                      user(){                          return this.$store.getters.getUser;                          },                      //当前评论所属文章的计算属性                      article(){                              return this.$store.getters.getArticle.data;                          }                      },              methods: {                  /**             * 点赞             */                  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('你已经赞过了哦~');                                  }                  },                  /**             * 提交评论             */                  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(() => {                                          //略,见源码                                          }).catch(() => {                                              this.$message({                                                  type: 'info',                                                  message: '已取消删除'                                                  });                                                  });                                                  } else {                                                      this.$confirm('此操作将永久删除此评论, 是否继续?', '提示', {                                                          confirmButtonText: '确定',                                                          cancelButtonText: '取消',                                                          type: 'warning'                                                          }).then(() => {                                                              //略,见源码                                                              }).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');                          },                          }                       }  </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 toMail($notifiable)  {      $url = env('APP_URL').'/art/' .  $this->comment->article_id . '?reply=comment&location=' . $this->comment->id;      return (new MailMessage)          ->line('你的文章有了新评论!')          ->action('查看评论', $url);  }

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

但是我不满足现状,我还想让用户点击邮件内的外链自动定位到目标评论或回复,解决办法就是给每个评论和回复添加锚点,如 ID 为 1 的评论标签的 id 就是"#comment1"。

<div class="comment" v-for="item in comments">              <div class="info" :id="'comment'+item.id">                  <img class="avatar" :src="item.fromAvatar" width="36" height="36"/>                  <div class="right">                      <div class="name">{{item.fromName}}                          <el-badge v-if="item.fromId == article.user_id" class="item">                              <i style="color: #E6A23C;" class="el-icon-star-on"></i>                          </el-badge>                      </div>                      <div class="date">{{item.date}}</div>                  </div>              </div>  <div>

新建一个 anchor() 方法:

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

如果地址栏中有 reply 参数的话就会拼接锚点,然后定位到目标锚点,这样点击邮件链接就能够自动滚动到文章底部的目标评论或者回复的位置了。