Laravel Vuejs 实战:开发知乎 (27)对答案进行点赞
1.建表
用户与回答点赞的关系表【多对多关系】
1 php artisan make:model Vote -m
1 <?php 2 3 use Illuminate\Database\Migrations\Migration; 4 use Illuminate\Database\Schema\Blueprint; 5 use Illuminate\Support\Facades\Schema; 6 7 class CreateVotesTable extends Migration 8 { 9 /** 10 * Run the migrations. 11 * 12 * @return void 13 */ 14 public function up() 15 { 16 Schema::create('votes', function (Blueprint $table) { 17 $table->bigIncrements('id'); 18 $table->unsignedBigInteger('user_id')->index()->comment("点赞用户的id"); 19 $table->unsignedBigInteger('answer_id')->index()->comment("被赞答案的id"); 20 $table->timestamps(); 21 }); 22 Schema::table('votes', function (Blueprint $table) { 23 //用户表中用户被删除的时候,同步删除votes表user_id列对应的数据行 24 $table 25 ->foreign('user_id')//本表有一个user_id列 26 ->references('id')//指向了id列 27 ->on('users')//users表中的那个id列 28 ->onDelete('cascade');//users表中id列里面任意一个id数据删除的时候,删除本votes表user_id列对应的数据行 29 30 //答案表中答案被删除的时候,同步删除votes表answer_id对应的数据行 31 $table 32 ->foreign('answer_id')//本表有一个answer_id列 33 ->references('id')//指向了id列 34 ->on('answers')//answers表中的那个id列 35 ->onDelete('cascade');//answers表中id列里面任意一个id数据删除的时候,删除本votes表answer_id列对应的数据行 36 }); 37 } 38 39 /** 40 * Reverse the migrations. 41 * 42 * @return void 43 */ 44 public function down() 45 { 46 Schema::dropIfExists('votes'); 47 } 48 } 49 50
1 php artisan migrate
参照:Migration Foreign Key Vs Eloquent Relationships in Laravel 强调一下,Eloquent关系和Migration必须同时创建,但是外键约束视情况而定。 Laravel Relationship - Foreign key delete
2.模型关联:
User.php中添加:
1 public function votes() 2 { 3 return $this->belongsToMany(Answer::class, 'votes')->withTimestamps(); 4 } 5
Answer.php中添加:
1 public function users() 2 { 3 return $this->belongsToMany(User::class, 'votes')->withTimestamps(); 4 } 5
注意 表名由于不是laravel默认的格式,所以要自己设置表名
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 44
最好在User.php中添加上voteAnswer方法,更方便:
1 public function voteAnswer($answer_id) 2 { 3 return $this->votes()->toggle($answer_id); 4 } 5
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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
3.视图层面:直接使用vue组件:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 <template> 2 <button :class="classObject" 3 @click="up" 4 v-text="text"> 5 </button> 6 </template> 7 8 <script> 9 export default { 10 props: ['answer'], 11 name: "UserVoteButton", 12 data() { 13 return { 14 voteable: true, 15 } 16 }, 17 computed: { 18 text() { 19 return this.voteable ? "点赞" : "取消赞"; 20 }, 21 classObject() { 22 return this.voteable ? "btn btn-sm btn-secondary" : "btn btn-sm btn-danger"; 23 }, 24 }, 25 mounted: function () { 26 let currentObj = this; 27 axios.post('/api/answers/vote/stats', {'answer': this.answer}) 28 .then(function (response) { 29 currentObj.voteable = response.data.voteable; 30 }) 31 .catch(function (e) { 32 console.log(e); 33 }); 34 }, 35 methods: { 36 up() { 37 let currentObj = this; 38 axios.post('/api/answers/vote/up', {'answer': this.answer}) 39 .then(function (response) { 40 currentObj.voteable = response.data.voteable; 41 } 42 ) 43 .catch(function (e) { 44 console.log(e); 45 }); 46 }, 47 //暂时不写踩的 48 down() { 49 let currentObj = this; 50 axios.post('/api/answers/vote/down', {'answer': this.answer}) 51 .then(function (response) { 52 currentObj.voteable = response.data.voteable; 53 } 54 ) 55 .catch(function (e) { 56 console.log(e); 57 }); 58 }, 59 } 60 } 61 </script> 62 63 <style scoped> 64 65 </style> 66 67
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 /** 31 * Next, we will create a fresh Vue application instance and attach it to 32 * the page. Then, you may begin adding components to this application 33 * or customize the JavaScript scaffolding to fit your unique needs. 34 */ 35 36 const app = new Vue({ 37 el: '#app', 38 }); 39 40
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 }}" class="float-right"></user-vote-button> 76 @endif 77 </div> 78 79 <div class="card-body"> 80 {!! $answer->content !!} 81 </div> 82 </div> 83 84 @empty 85 86 @endforelse 87 </div> 88 89 <div class="col-md-3"> 90 <div class="card"> 91 <div class="card-header"> 92 <h2> {{ $question->followers_count }}</h2> 93 <span>关注者</span> 94 </div> 95 <div class="card-body"> 96 <question-follow-button question="{{$question->id}}"id}}"> 97 </question-follow-button> 98 </div> 99 </div> 100 101 <div class="card mt-4"> 102 <div class="card-header"> 103 <h2> 提问者 </h2> 104 </div> 105 <div class="card-body"> 106 @include('users._small_icon',['userable'=>$question]) 107 </div> 108 @include('users._user_stats') 109 </div> 110 </div> 111 112 113 </div> 114 </div> 115 @endsection 116 @section('footer-js') 117 @include('questions._footer_js') 118 @endsection 119 120
api.php:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 <?php 2 3 use Illuminate\Http\Request; 4 5 /* 6 |-------------------------------------------------------------------------- 7 | API Routes 8 |-------------------------------------------------------------------------- 9 | 10 | Here is where you can register API routes for your application. These 11 | routes are loaded by the RouteServiceProvider within a group which 12 | is assigned the "api" middleware group. Enjoy building your API! 13 | 14 */ 15 16 Route::middleware('auth:api')->get('/user', function (Request $request) { 17 return $request->user(); 18 }); 19 20 Route::middleware('api')->get('/topics', function (Request $request) { 21 $query = $request->query('q'); 22 return \App\Topic::query()->where('name', 'like', '%' . $query . '%')->get(); 23 }); 24 #region 问题关注 25 //加载页面时取关注状态 26 Route::middleware('auth:api')->post('/questions/follow/stats', 'QuestionController@getFollowStats'); 27 //执行关注/取关操作 28 Route::middleware('auth:api')->post('/questions/follow', 'QuestionController@followThroughApi'); 29 #endregion 30 31 #region 用户关注 32 //加载页面时取关注状态 33 Route::middleware('auth:api')->post('/users/follow/stats', 'FollowerController@getFollowStats'); 34 //执行关注/取关操作 35 Route::middleware('auth:api')->post('/users/follow', 'FollowerController@followThroughApi'); 36 37 #endregion 38 39 40 #region 41 //加载页面时取赞状态 42 Route::middleware('auth:api')->post('/answers/vote/stats', 'VoteController@getVoteStats'); 43 //执行赞/取消赞操作 44 Route::middleware('auth:api')->post('/answers/vote/up', 'VoteController@voteUpThroughApi'); 45 #endregion 46 47
4.控制器:
1 php artisan make:controller VoteController
VoteController.php:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 <?php 2 3 namespace App\Http\Controllers; 4 5 use App\Answer; 6 use Illuminate\Http\Request; 7 8 class VoteController extends Controller 9 { 10 // 11 public function __construct() 12 { 13 $this->middleware('auth'); 14 } 15 16 17 public function getVoteStats(Request $request) 18 { 19 $answer = Answer::find($request->get('answer')); 20 21 $user = auth()->user(); 22 23 //是否可以点赞 24 return response()->json( 25 [ 26 'voteable' => !($user->votes->contains('id', $answer->id)) 27 ] 28 ); 29 } 30 31 public function voteUpThroughApi(Request $request) 32 { 33 $answer = Answer::find($request->get('answer')); 34 35 $user = auth()->user(); 36 37 $user->voteAnswer($answer->id); 38 39 //是否可以点赞 40 return response()->json( 41 [ 42 'voteable' => !($user->votes->contains('id', $answer->id)) 43 ] 44 ); 45 } 46 } 47 48
5.如果要对赞进行计数:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 }}" vote_count="{{ $answer->userVotes->count() }}" class="float-right"></user-vote-button> 76 @endif 77 </div> 78 79 <div class="card-body"> 80 {!! $answer->content !!} 81 </div> 82 </div> 83 84 @empty 85 86 @endforelse 87 </div> 88 89 <div class="col-md-3"> 90 <div class="card"> 91 <div class="card-header"> 92 <h2> {{ $question->followers_count }}</h2> 93 <span>关注者</span> 94 </div> 95 <div class="card-body"> 96 <question-follow-button question="{{$question->id}}"id}}"> 97 </question-follow-button> 98 </div> 99 </div> 100 101 <div class="card mt-4"> 102 <div class="card-header"> 103 <h2> 提问者 </h2> 104 </div> 105 <div class="card-body"> 106 @include('users._small_icon',['userable'=>$question]) 107 </div> 108 @include('users._user_stats') 109 </div> 110 </div> 111 112 113 </div> 114 </div> 115 @endsection 116 @section('footer-js') 117 @include('questions._footer_js') 118 @endsection 119 120
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 <template> 2 <button :class="classObject" 3 @click="up" 4 v-text="text"> 5 </button> 6 </template> 7 8 <script> 9 export default { 10 props: ['answer', 'vote_count'], 11 name: "UserVoteButton", 12 data() { 13 return { 14 voteable: true, 15 vote_count: this.vote_count, 16 } 17 }, 18 computed: { 19 text() { 20 return this.vote_count; 21 }, 22 classObject() { 23 return this.voteable ? "btn btn-sm btn-secondary" : "btn btn-sm btn-danger"; 24 }, 25 }, 26 mounted: function () { 27 let currentObj = this; 28 axios.post('/api/answers/vote/stats', {'answer': this.answer}) 29 .then(function (response) { 30 currentObj.voteable = response.data.voteable; 31 currentObjvote_count = response.data.vote_count; 32 }) 33 .catch(function (e) { 34 console.log(e); 35 }); 36 }, 37 methods: { 38 up() { 39 let currentObj = this; 40 axios.post('/api/answers/vote/up', {'answer': this.answer}) 41 .then(function (response) { 42 currentObj.voteable = response.data.voteable; 43 currentObj.vote_count = response.data.vote_count; 44 } 45 ) 46 .catch(function (e) { 47 console.log(e); 48 }); 49 }, 50 //暂时不写踩的 51 down() { 52 let currentObj = this; 53 axios.post('/api/answers/vote/down', {'answer': this.answer}) 54 .then(function (response) { 55 currentObj.voteable = response.data.voteable; 56 } 57 ) 58 .catch(function (e) { 59 console.log(e); 60 }); 61 }, 62 } 63 } 64 </script> 65 66 <style scoped> 67 68 </style> 69 70
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 <?php 2 3 namespace App\Http\Controllers; 4 5 use App\Answer; 6 use Illuminate\Http\Request; 7 8 class VoteController extends Controller 9 { 10 // 11 public function __construct() 12 { 13 $this->middleware('auth'); 14 } 15 16 17 public function getVoteStats(Request $request) 18 { 19 $answer = Answer::find($request->get('answer')); 20 21 $user = auth()->user(); 22 23 //是否可以点赞 24 return response()->json( 25 [ 26 'voteable' => !($user->votes->contains('id', $answer->id)), 27 'vote_count' => $answer->userVotes->count(), 28 ] 29 ); 30 } 31 32 public function voteUpThroughApi(Request $request) 33 { 34 $answer = Answer::find($request->get('answer')); 35 36 $user = auth()->user(); 37 38 $user->voteAnswer($answer->id); 39 40 //是否可以点赞 41 return response()->json( 42 [ 43 'voteable' => !($user->votes->contains('id', $answer->id)), 44 'vote_count' => $answer->userVotes->count(), 45 ] 46 ); 47 } 48 } 49 50