写在前面
这篇文章严格来讲是将已有的仿简书二级评论系统和 Laravel、Vue 进行结合并改进,例如添加邮件通知。前人栽树后人乘凉,评论系统的数据结构和 Vue 模板详情见下面这篇文章,本文不在赘述。
项目源码
前端评论模板: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);
}
这样我们在收到评论或回复时就能够及时收到邮件通知啦!
更多信息
关注下我的微信公众号就是对我创作的最大支持~