Laravel Vuejs 实战:开发知乎 (30-32)评论功能

1.执行命令:

  1 php artisan make:model Comment -mc

2.数据库迁移文件:

****_create_comments_table.php文件:

评论与之前的不同在于,评论可以评论问题,也可以评论答案,还可以评论其他评论;也就是多态多对多。

可以参考: Laravel Polymorphic Relationship Example

Laravel Many to Many Polymorphic Relationship Tutorial

其次,评论可以嵌套,所以需要注意层级,上层评论的id,以及评论隐藏状态。

  1 <?php
  2 
  3 use Illuminate\Database\Migrations\Migration;
  4 use Illuminate\Database\Schema\Blueprint;
  5 use Illuminate\Support\Facades\Schema;
  6 
  7 class CreateCommentsTable extends Migration
  8 {
  9     /**
 10      * Run the migrations.
 11      *
 12      * @return void
 13      */
 14     public function up()
 15     {
 16         Schema::create('comments', function (Blueprint $table) {
 17             $table->bigIncrements('id');
 18             $table->unsignedBigInteger('user_id')->comment("发出评论的用户的id");
 19             $table->text('content')->comment("评论的内容");
 20             $table->unsignedBigInteger('commentable_id')->comment("被评论的对象的id");
 21             $table->string('commentable_type')->comment("被评论的类型 问题或者答案或者评论");
 22             $table->unsignedBigInteger('parent_id')->nullable()->comment("嵌套评论的上级id");
 23             $table->unsignedSmallInteger('level')->default(1)->comment("评论属于第几层");
 24             $table->string('is_hidden', 8)->default("F")->comment("是否隐藏状态");
 25             $table->timestamps();
 26         });
 27     }
 28 
 29     /**
 30      * Reverse the migrations.
 31      *
 32      * @return void
 33      */
 34     public function down()
 35     {
 36         Schema::dropIfExists('comments');
 37     }
 38 }
 39 
 40 
CreateCommentsTable.php

执行:

  1 php artisan migrate

3.comment模型初始设置:

  1 //表名
  2 protected $table = 'comments';
  3 
  4 //必须初始赋值的
  5 protected $fillable = ['user_id', 'content', 'commentable_id', 'commentable_type'];
  6 

4.模型关联

多态多对多

Comment.php中添加:

  1 public function commentable()
  2 {
  3     return $this->morphTo();
  4 }
  5 

Answer.php中添加:

  1 public function comments()
  2 {
  3     return $this->morphMany(Comment::class, 'commentable');
  4 }
  5 
  1 <?php
  2 
  3 namespace App;
  4 
  5 use App\Models\Question;
  6 use Illuminate\Database\Eloquent\Model;
  7 use Illuminate\Database\Eloquent\SoftDeletes;
  8 
  9 class Answer extends Model
 10 {
 11     #region 支持软删除添加
 12     use SoftDeletes;
 13     protected $dates = ['deleted_at'];
 14 
 15     #endregion
 16 
 17     protected $fillable = ['user_id', 'question_id', 'content'];
 18 
 19     /** 一个回答只有一个回答主人
 20      * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
 21      */
 22     public function user()
 23     {
 24         return $this->belongsTo(User::class);
 25     }
 26 
 27     /** 一个回答只针对一个问题
 28      * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
 29      */
 30     public function question()
 31     {
 32         return $this->belongsTo(Question::class);
 33     }
 34 
 35     /**
 36      * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
 37      */
 38     public function userVotes()
 39     {
 40         return $this->belongsToMany(User::class, 'votes')->withTimestamps();
 41     }
 42 
 43     public function comments()
 44     {
 45         return $this->morphMany(Comment::class, 'commentable');
 46     }
 47 }
 48 
 49 
Answer.php

Question.php中添加:

  1 public function comments()
  2 {
  3     return $this->morphMany(Comment::class, 'commentable');
  4 }
  5 
  1 <?php
  2 
  3 namespace App\Models;
  4 
  5 use App\Answer;
  6 use App\Comment;
  7 use App\Topic;
  8 use App\User;
  9 use Illuminate\Database\Eloquent\Model;
 10 use Illuminate\Database\Eloquent\SoftDeletes;
 11 
 12 class Question extends Model
 13 {
 14     //软删除 添加
 15     use SoftDeletes;
 16     //
 17     protected $fillable = ['title', 'content', 'user_id'];
 18     //支持软删除 添加
 19     protected $dates = ['deleted_at'];
 20 
 21     public function topics()
 22     {
 23         return $this->belongsToMany(
 24             Topic::class,
 25             'questions_topics' //表名我设置的是questions_topics,可能不是系统自动解析的question_topic
 26         )->withTimestamps();//withTimestamps操作questions_topics表中create_at及updated_at字段的
 27     }
 28 
 29     public function user()
 30     {
 31         return $this->belongsTo(User::class);
 32     }
 33 
 34     /** scope+请求名命名的
 35      * @return bool
 36      */
 37     public function scopePublished($query)
 38     {
 39         return $query->where('is_hidden', 'F');//等于F表示不隐藏
 40     }
 41 
 42 
 43     /** 一个问题有多个回答
 44      * @return \Illuminate\Database\Eloquent\Relations\HasMany
 45      */
 46     public function answers()
 47     {
 48         return $this->hasMany(Answer::class);
 49     }
 50 
 51 
 52     public function followUsers()
 53     {
 54         //默认表名 可以不设置后面三个参数,自定义表名需要设置
 55         return $this->belongsToMany(Question::class, 'users_questions', 'user_id', 'question_id')->withTimestamps();
 56     }
 57 
 58     public function comments()
 59     {
 60         return $this->morphMany(Comment::class, 'commentable');
 61     }
 62 
 63 }
 64 
 65 
Question.php

5.view部分 vue组件

  1 <template>
  2     <div>
  3         <button class="btn btn-sm btn-secondary ml-2"
  4                 @click="showCommentsForm" v-text="showCommentText">
  5         </button>
  6 
  7         <div class="modal fade" :id="id" tabindex="-1" role="dialog">
  8             <div class="modal-dialog">
  9                 <div class="modal-content">
 10                     <div class="modal-header">
 11                         <h4 class="modal-title">
 12                             评论
 13                         </h4>
 14                         <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
 15                     </div>
 16 
 17                     <div class="modal-body">
 18                         <div v-if="comments.length>0">
 19                             <div class="card" v-for="comment in comments">
 20                                 <div class="card-body">
 21                                     <div class="row">
 22                                         <div class="col-md-2">
 23                                             <img :src="comment.user.avatar" class="img img-rounded img-fluid"
 24                                                  :alt="comment.user.name">
 25                                             <p class="text-secondary text-center"> {{
 26                                                 comment.created_at }}</p>
 27                                         </div>
 28                                         <div class="col-md-10">
 29                                             <p>
 30                                                 <a class="float-left" href="#">
 31                                                     <strong>{{ comment.user.name}}</strong></a>
 32                                             </p>
 33                                             <div class="clearfix"></div>
 34                                             <p> {{ comment.content }}</p>
 35                                             <p>
 36                                                 <a class="float-right btn btn-outline-primary ml-2"> <i
 37                                                     class="fa fa-reply"></i> 回复</a>
 38                                                 <a class="float-right btn text-white btn-danger"> <i
 39                                                     class="fa fa-heart"></i> 点赞</a>
 40                                             </p>
 41                                         </div>
 42                                     </div>
 43                                 </div>
 44                                 <!--                                  <div class="card card-inner">-->
 45                                 <!--                                       <div class="card-body">-->
 46                                 <!--                                           <div class="row">-->
 47                                 <!--                                               <div class="col-md-2">-->
 48                                 <!--                                                   <img src="https://image.ibb.co/jw55Ex/def_face.jpg"-->
 49                                 <!--                                                        class="img img-rounded img-fluid"/>-->
 50                                 <!--                                                   <p class="text-secondary text-center">15 Minutes Ago</p>-->
 51                                 <!--                                               </div>-->
 52                                 <!--                                               <div class="col-md-10">-->
 53                                 <!--                                                   <p><a-->
 54                                 <!--                                                       href="https://maniruzzaman-akash.blogspot.com/p/contact.html"><strong>Maniruzzaman-->
 55                                 <!--                                                       Akash</strong></a></p>-->
 56                                 <!--                                                   <p>Lorem Ipsum is simply dummy text of the pr make but also the leap-->
 57                                 <!--                                                       into electronic typesetting, remaining essentially unchanged. It was-->
 58                                 <!--                                                       popularised in the 1960s with the release of Letraset sheets-->
 59                                 <!--                                                       containing Lorem Ipsum passages, and more recently with desktop-->
 60                                 <!--                                                       publishing software like Aldus PageMaker including versions of Lorem-->
 61                                 <!--                                                       Ipsum.</p>-->
 62                                 <!--                                                   <p>-->
 63                                 <!--                                                       <a class="float-right btn btn-outline-primary ml-2"> <i-->
 64                                 <!--                                                           class="fa fa-reply"></i> Reply</a>-->
 65                                 <!--                                                       <a class="float-right btn text-white btn-danger"> <i-->
 66                                 <!--                                                           class="fa fa-heart"></i> Like</a>-->
 67                                 <!--                                                   </p>-->
 68                                 <!--                                               </div>-->
 69                                 <!--                                           </div>-->
 70                                 <!--                                       </div>-->
 71                                 <!--                                   </div>-->
 72                             </div>
 73                         </div>
 74                         <input class="form-control" v-model="postComment" v-if="!success"></input>
 75                         <button type="button" class="btn btn-success" @click="store" v-if="!success">发送</button>
 76                         <div class="alert alert-success" v-if="success">评论发送成功!</div>
 77                     </div>
 78 
 79                     <!-- Modal Actions -->
 80                     <div class="modal-footer">
 81                         <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
 82                     </div>
 83                 </div>
 84             </div>
 85         </div>
 86     </div>
 87 
 88 </template>
 89 
 90 <script>
 91     export default {
 92         props: ['type', 'commentable_id'],
 93         name: "Comments.vue",
 94         data: function () {
 95             return {
 96                 comments: [],
 97                 postComment: null,
 98                 success: false,
 99             }
100         },
101         computed: {
102             id() {
103                 return 'model-comment' + '-' + this.type + '-' + this.commentable_id;
104             },
105             showCommentText() {
106                 return this.comments.length + "条评论";
107             },
108         },
109         mounted() {
110             this.getComments();
111         },
112         methods: {
113             showCommentsForm() {
114                 this.getComments();
115                 $('#' + this.id).modal('show');//显示评论框
116             },
117             getComments() {
118                 let currentObject = this;
119                 axios.get('/api/' + this.type + '/' + this.commentable_id + '/comments').then(function (response) {
120                     currentObject.comments = response.data.comments;
121                 }).catch(function (e) {
122                     console.log(e);
123                 }).finally(function () {
124 
125                 });
126             },
127             store() {
128                 let currentObject = this;
129                 axios.post('/api/comments', {
130                     'type': this.type,
131                     'commentable_id': this.commentable_id,
132                     'postComment': this.postComment
133                 }).then(function (response) {
134                     currentObject.comments.push(response.data.comment[0]);
135                 }).catch(function (e) {
136                     console.log(e);
137                 }).finally(function () {
138 
139                 });
140             }
141         }
142     }
143 </script>
144 
145 <style scoped>
146 
147 </style>
148 
149 
Comments.vue
  1 @extends('layouts.app')
  2 @section('content')
  3     <div class="container">
  4         <div class="row">
  5             <div class="col-md-8 col-md offset-1">
  6                 {{--问题--}}
  7                 <div class="card">
  8                     <div class="card-header">
  9                         {{ $question->title }}
 10 
 11                         @foreach(['success','warning','danger'] as $info)
 12                             @if(session()->has($info))
 13                                 <div class="alert alert-{{$info}}">{{ session()->get($info) }}</div>
 14                             @endif
 15                         @endforeach
 16 
 17                         @can('update',$question)
 18                             <a href="{{ route('questions.edit',$question) }}" class="btn btn-warning">编辑</a>
 19                         @endcan
 20 
 21                         @can('destroy',$question)
 22                             <form action="{{ route('questions.destroy',$question) }}" method="post">
 23                                 @csrf
 24                                 @method('DELETE')
 25                                 <button type="submit" class="btn btn-danger">删除</button>
 26                             </form>
 27                         @endcan
 28 
 29                         @forelse($question->topics as $topic)
 30                             <button class="btn btn-secondary float-md-right m-1">{{ $topic->name }}</button>
 31                         @empty
 32                             <p class="text text-warning float-md-right"> "No Topics"</p>
 33                         @endforelse
 34 
 35                         <p class="text text-info float-md-right"> 已有{{ count($question->answers) }}个回答</p>
 36 
 37                     </div>
 38                     <div class="card-body">
 39                         {!! $question->content !!}
 40                     </div>
 41                 </div>
 42 
 43 
 44                 {{--回答提交form--}}
 45                 {{--只有登录用户可以提交回答--}}
 46                 @if(auth()->check())
 47                     <div class="card mt-2">
 48                         <div class="card-header">
 49                             提交回答
 50                         </div>
 51                         <div class="card-body">
 52                             <form action="{{ route('answers.store',$question) }}" method="post">
 53                             @csrf
 54                             <!-- 回答编辑器容器 -->
 55                                 <script id="container" name="content" type="text/plain"
 56                                         style="width: 100%;height: 200px">{!! old('content') !!}</script>
 57                                 <p class="text text-danger"> @error('content') {{ $message }} @enderror </p>
 58                                 <!--提交按钮-->
 59                                 <button type="submit" class="btn btn-primary float-md-right mt-2">提交回答</button>
 60                             </form>
 61                         </div>
 62                     </div>
 63                 @else
 64                     {{--显示请登录--}}
 65                     <a href="{{ route('login') }}" class="btn btn-success btn-block mt-4">登录提交答案</a>
 66                 @endif
 67                 {{--展示答案--}}
 68                 @forelse($question->answers as $answer)
 69                     <div class="card mt-4">
 70                         <div class="card-header">
 71                             @include('users._small_icon',['userable'=>$answer])
 72                             <span class="float-right text text-info text-center">
 73                                 {{ $answer->updated_at->diffForHumans() }}</span>
 74                             @if(auth()->check())
 75                                 <user-vote-button answer="{{ $answer->id }}"
 76                                                   vote_count="{{ $answer->userVotes->count() }}"
 77                                                   class="float-right"></user-vote-button>
 78                             @endif
 79                         </div>
 80 
 81                         <div class="card-body">
 82                             {!!  $answer->content  !!}
 83                         </div>
 84                         <div class="card-footer">
 85                             <comments type="answer" commentable_id="{{  $answer->id }}"></comments>
 86                         </div>
 87                     </div>
 88 
 89                 @empty
 90 
 91                 @endforelse
 92             </div>
 93 
 94             <div class="col-md-3">
 95                 <div class="card">
 96                     <div class="card-header">
 97                         <h2> {{ $question->followers_count }}</h2>
 98                         <span>关注者</span>
 99                     </div>
100                     <div class="card-body">
101                         <question-follow-button question="{{$question->id}}"id}}">
102                         </question-follow-button>
103                     </div>
104                 </div>
105 
106                 <div class="card mt-4">
107                     <div class="card-header">
108                         <h2> 提问者 </h2>
109                     </div>
110                     <div class="card-body">
111                         @include('users._small_icon',['userable'=>$question])
112                     </div>
113                     @include('users._user_stats')
114                 </div>
115             </div>
116 
117 
118         </div>
119     </div>
120 @endsection
121 @section('footer-js')
122     @include('questions._footer_js')
123 @endsection
124 
125 
show.blade.php
  1 /**
  2  * First we will load all of this project's JavaScript dependencies which
  3  * includes Vue and other libraries. It is a great starting point when
  4  * building robust, powerful web applications using Vue and Laravel.
  5  */
  6 
  7 require('./bootstrap');
  8 require('../../vendor/select2/select2/dist/js/select2.js');
  9 // 将views/vendor/ueditor/assets.blade.php中的引用换到本处
 10 require('../../public/vendor/ueditor/ueditor.config.js');
 11 require('../../public/vendor/ueditor/ueditor.all.js');
 12 
 13 window.Vue = require('vue');
 14 
 15 /**
 16  * The following block of code may be used to automatically register your
 17  * Vue components. It will recursively scan this directory for the Vue
 18  * components and automatically register them with their "basename".
 19  *
 20  * Eg. ./components/ExampleComponent.vue -> <example-component></example-component>
 21  */
 22 
 23 // const files = require.context('./', true, /\.vue$/i)
 24 // files.keys().map(key => Vue.component(key.split('/').pop().split('.')[0], files(key).default))
 25 
 26 // Vue.component('example-component', require('./components/ExampleComponent.vue').default);
 27 Vue.component('question-follow-button', require('./components/QuestionFollowButton').default);
 28 Vue.component('user-follow-button', require('./components/UserFollowButton').default);
 29 Vue.component('user-vote-button', require('./components/UserVoteButton').default);
 30 Vue.component('send-message', require('./components/SendMessage').default);
 31 Vue.component('comments', require('./components/Comments').default);
 32 /**
 33  * Next, we will create a fresh Vue application instance and attach it to
 34  * the page. Then, you may begin adding components to this application
 35  * or customize the JavaScript scaffolding to fit your unique needs.
 36  */
 37 
 38 const app = new Vue({
 39     el: '#app',
 40 });
 41 
 42 
app.js

6.CommentContoller及路由api.php

api.php:

  1 #region
  2 Route::middleware('api')->get('/answer/{id}/comments', 'CommentController@showAnswerComment');
  3 Route::middleware('api')->get('/question/{id}/comments', 'CommentController@showQuestionComment');
  4 
  5 Route::middleware('auth:api')->post('/comments', 'CommentController@store');
  6 #endregion
  7 
  1 <?php
  2 
  3 namespace App\Http\Controllers;
  4 
  5 use App\Comment;
  6 use Illuminate\Http\Request;
  7 use Illuminate\Database\Eloquent\Builder;
  8 
  9 class CommentController extends Controller
 10 {
 11     //
 12 
 13     public function showAnswerComment($id)
 14     {
 15         //https://laravel.com/docs/5.8/eloquent-relationships#querying-polymorphic-relationships
 16         $comments = Comment::whereHasMorph('commentable', ['App\Answer'], function (Builder $query) use ($id) {
 17             $query->where('id', '=', $id);
 18         })->with('user')->get();
 19 
 20         return response()->json(
 21             [
 22                 'comments' => $comments,
 23             ]
 24         );
 25     }
 26 
 27     public function showQuestionComment($id)
 28     {
 29         $comments = Comment::query()->whereHasMorph('commentable', 'App\Models\Question', function (Builder $query) use ($id) {
 30             $query->where('id', $id);
 31         })->with('user')->get();
 32         return response()->json(
 33             [
 34                 'comments' => $comments,
 35             ]
 36         );
 37     }
 38 
 39     public function store(Request $request)
 40     {
 41         $type = ($request->get('type') === 'answer') ? 'App\Answer' : 'App\Models\Question';
 42         $comment = Comment::create([
 43             'commentable_type' => $type,
 44             'commentable_id' => $request->get('commentable_id'),
 45             'user_id' => auth()->user()->id,
 46             'content' => $request->get('postComment'),
 47         ]);
 48         $comment = Comment::query()->where('id', $comment->id)->with('user')->get();
 49         return response()->json(
 50             [
 51                 'comment' => $comment,
 52             ]
 53         );
 54     }
 55 
 56 }
 57 
 58 
CommentController.php

7.添加comment与用户模型的关联:

Comment.php中:

  1 public function user()
  2 {
  3     return $this->belongsTo(User::class);
  4 }
  5 
  1 <?php
  2 
  3 namespace App;
  4 
  5 use Illuminate\Database\Eloquent\Model;
  6 
  7 class Comment extends Model
  8 {
  9     //表名
 10     protected $table = 'comments';
 11 
 12     //必须初始赋值的
 13     protected $fillable = ['user_id', 'content', 'commentable_id', 'commentable_type'];
 14 
 15 
 16     public function commentable()
 17     {
 18         return $this->morphTo();
 19     }
 20 
 21     public function user()
 22     {
 23         return $this->belongsTo(User::class);
 24     }
 25 }
 26 
 27 
Comment.php

User.php中:

  1 public function comments()
  2 {
  3     return $this->hasMany(Comment::class);
  4 }
  5 
  1 <?php
  2 
  3 namespace App;
  4 
  5 use App\Models\Question;
  6 use Illuminate\Contracts\Auth\MustVerifyEmail;
  7 use Illuminate\Database\Eloquent\SoftDeletes;
  8 use Illuminate\Foundation\Auth\User as Authenticatable;
  9 use Illuminate\Notifications\Notifiable;
 10 
 11 class User extends Authenticatable implements MustVerifyEmail
 12 {
 13     use Notifiable;
 14     #region 支持软删除
 15     use SoftDeletes;
 16     protected $dates = ['deleted_at'];
 17     #endregion
 18     /**
 19      * The attributes that are mass assignable.
 20      *
 21      * @var array
 22      */
 23     protected $fillable = [
 24         'name', 'email', 'password', 'avatar', 'activation_token', 'api_token'
 25     ];
 26 
 27     /**
 28      * The attributes that should be hidden for arrays.
 29      *
 30      * @var array
 31      */
 32     protected $hidden = [
 33         'password', 'remember_token',
 34     ];
 35 
 36     /**
 37      * The attributes that should be cast to native types.
 38      *
 39      * @var array
 40      */
 41     protected $casts = [
 42         'email_verified_at' => 'datetime',
 43     ];
 44 
 45 
 46     /**添加用户模型和问题模型的模型关联
 47      * @return \Illuminate\Database\Eloquent\Relations\HasMany
 48      */
 49     public function questions()
 50     {
 51         return $this->hasMany(Question::class);
 52     }
 53 
 54 
 55     /** 添加用户模型和回答模型的模型关联 一个用户可以有多个回答
 56      * @return \Illuminate\Database\Eloquent\Relations\HasMany
 57      */
 58     public function answers()
 59     {
 60         return $this->hasMany(Answer::class);
 61     }
 62 
 63 
 64     public function followQuestions()
 65     {
 66         //默认表名 可以不设置后面三个参数,自定义表名需要设置
 67         return $this->belongsToMany(Question::class, 'users_questions', 'question_id', 'user_id')->withTimestamps();
 68     }
 69 
 70 
 71     /** 用户的粉丝
 72      * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
 73      */
 74     public function followers()
 75     {
 76 
 77         return $this->belongsToMany
 78         (
 79             self::class,
 80             'followers',
 81             'user_id', //foreignPivotKey:当前模型在中间表的字段(当前模型类的外键) //【当前模型是leader】的外键id
 82             'follower_id'//relatedPivotKey:另一模型在中间表的字段(另一模型类的外键)
 83         )->withTimestamps();
 84     }
 85 
 86 
 87     /** 用户关注的作者
 88      * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
 89      */
 90     public function followings()
 91     {
 92         return $this->belongsToMany
 93         (
 94             self::class,
 95             'followers',
 96             'follower_id',//foreignPivotKey:当前模型在中间表的字段(当前模型类的外键) //【当前模型是粉丝】的外键id
 97             'user_id'//relatedPivotKey:另一模型在中间表的字段(另一模型类的外键)
 98         )
 99             ->withTimestamps();
100     }
101 
102 
103     /**
104      * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
105      */
106     public function votes()
107     {
108         return $this->belongsToMany(Answer::class, 'votes')->withTimestamps();
109     }
110 
111 
112     /**
113      * @param $answer_id
114      * @return array
115      */
116     public function voteAnswer($answer_id)
117     {
118         return $this->votes()->toggle($answer_id);
119     }
120 
121 
122     public function messages()
123     {
124         return $this->hasMany(Message::class, 'to_user_id');
125     }
126 
127     public function comments()
128     {
129         return $this->hasMany(Comment::class);
130     }
131 
132 }
133 
134 
User.php


8.Repository 模式重构评论 :省略

posted @ 2020-03-03 15:12  dzkjz  阅读(446)  评论(0)    收藏  举报