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 参数的话就会拼接锚点,然后定位到目标锚点,这样点击邮件链接就能够自动滚动到文章底部的目标评论或者回复的位置了。