路由
// 书籍详情页
Route::get('/book/{id}', [\App\Http\Controllers\HomeController::class, 'detail'])
->name('book.detail');
// 书籍评论
Route::post('/save-book-review', [\App\Http\Controllers\HomeController::class, 'saveReview'])
->name('book.saveReview');
Route::group(['middleware' => 'auth'], function () {
// 评论页
Route::get('reviews', [\App\Http\Controllers\ReviewController::class, 'index'])
->name('account.reviews');
// 评论编辑
Route::get('reviews/{id}', [\App\Http\Controllers\ReviewController::class, 'edit'])
->name('account.reviews.edit');
// 执行评论编辑
Route::post('reviews/{id}', [\App\Http\Controllers\ReviewController::class, 'updateReview'])
->name('account.reviews.updateReview');
// 删除评论
Route::post('delete-review', [\App\Http\Controllers\ReviewController::class, 'deleteReview'])
->name('account.reviews.deleteReview');
});
创建评论迁移文件
php artisan make:migration create_reviews_table
编写迁移文件
2024_11_19_154431_create_reviews_table.php
public function up(): void
{
Schema::create('reviews', function (Blueprint $table) {
$table->id();
$table->text('review');
$table->integer('rating');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('book_id')->constrained()->onDelete('cascade');
$table->integer('status')->default(0);
$table->timestamps();
});
}
执行迁移
php artisan migrate
评论模型
php artisan make:model Review
模型关系定义
App/Models/Book.php
// 书籍下的所有评论
public function reviews()
{
return $this->hasMany(Review::class);
}
App/Models/Review.php
// 每条评论属于用户
public function user()
{
return $this->belongsTo(User::class);
}
// 每条评论下的书籍
public function book()
{
return $this->belongsTo(Book::class);
}
书籍详情页加载评论
App/Http/Controllers/HomeController.php
<?php
namespace App\Http\Controllers;
use App\Models\Book;
use App\Models\Review;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
class HomeController extends Controller
{
// 显示首页书籍详情页
public function detail($id)
{
// 查询当前书籍详情页内容和评论
$book = Book::with(['reviews.user', 'reviews' => function ($query) {
$query->where('status', 1);
}])->findOrFail($id);
// 如果书籍是关闭状态就404
if ($book->status == 0) {
abort(404);
}
// 生成读者还喜欢的随机书籍 take(3)读取最近三条信息 inRandomOrder()随机排序
$relatedBooks = Book::where('status', 1)->take(3)->where('id', '!=', $id)->inRandomOrder()->get();
return view('book-detail', compact('book', 'relatedBooks'));
}
// 书籍评论
public function saveReview(Request $request)
{
$validator = Validator::make($request->all(), [
'review' => 'required|min:10',
'rating' => 'required',
]);
// 如果验证失败,我们将返回错误
if ($validator->fails()) {
return response()->json([
'status' => false,
'errors' => $validator->errors()
]);
}
// 评论数
$countReview = Review::where('user_id', Auth::user()->id)->where('book_id', $request->book_id)->count();
if ($countReview > 0) {
session()->flash('error', '您已经提交了评论');
return response()->json([
'status' => true,
]);
}
$review = new Review();
$review->review = $request->review;
$review->rating = $request->rating;
$review->user_id = Auth::user()->id;
$review->book_id = $request->book_id;
$review->save();
session()->flash('success', '已成功提交');
return response()->json([
'status' => true,
]);
}
}
resources/views/book-detail.blade.php
@extends('layouts.app')
@section('title', '书籍详情页')
@section('content')
<div class="container mt-3 ">
<div class="row justify-content-center d-flex mt-5">
<div class="col-md-12">
<a href="{{ route('home') }}" class="text-decoration-none text-dark ">
<i class="fa fa-arrow-left" aria-hidden="true"></i> <strong>返回首页</strong>
</a>
<div class="row mt-4">
<div class="col-md-4">
@if($book->image != '')
<img src="{{ asset('uploads/books/thumb/' .$book->image)}}" alt=""
class="card-img-top">
@else
<img src="https://placehold.co/990x1400?text=No Image" alt=""
class="card-img-top">
@endif
</div>
<div class="col-md-8">
{{-- 表单验证文件 --}}
@include('layouts.message')
<h3 class="h2 mb-3">{{ $book->title }}</h3>
<div class="h4 text-muted">{{ $book->author }}</div>
<div class="star-rating d-inline-flex ml-2" title="">
<span class="rating-text theme-font theme-yellow">5.0</span>
<div class="star-rating d-inline-flex mx-2" title="">
<div class="back-stars ">
<i class="fa fa-star " aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<div class="front-stars" style="width: 100%">
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
</div>
</div>
</div>
<span class="theme-font text-muted">(0 评论)</span>
</div>
<div class="content mt-3">
{{ $book->description }}
</div>
<div class="col-md-12 pt-2">
<hr>
</div>
<div class="row mt-4">
<div class="col-md-12">
<h2 class="h3 mb-4">读者还喜欢</h2>
</div>
@if($relatedBooks->isNotEmpty())
@foreach($relatedBooks as $relatedBook)
<div class="col-md-4 col-lg-4 mb-4">
<div class="card border-0 shadow-lg">
<a href="{{ route("book.detail",$relatedBook->id) }}">
@if($relatedBook->image != '')
<img src="{{ asset('uploads/books/thumb/' .$relatedBook->image)}}"
alt=""
class="card-img-top">
@else
<img src="https://placehold.co/990x1400?text=No Image" alt=""
class="card-img-top">
@endif
</a>
<div class="card-body">
<h3 class="h4 heading"><a
href="{{ route("book.detail",$relatedBook->id) }}">{{ $relatedBook->title }}</a>
</h3>
<p>{{ $relatedBook->author }}</p>
<div class="star-rating d-inline-flex ml-2" title="">
<span class="rating-text theme-font theme-yellow">0.0</span>
<div class="star-rating d-inline-flex mx-2" title="">
<div class="back-stars ">
<i class="fa fa-star " aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<div class="front-stars" style="width: 70%">
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
</div>
</div>
</div>
<span class="theme-font text-muted">(0)</span>
</div>
</div>
</div>
</div>
@endforeach
@else
<div>
未找到评论~~
</div>
@endif
</div>
<div class="col-md-12 pt-2">
<hr>
</div>
<div class="row pb-5">
<div class="col-md-12 mt-4">
<div class="d-flex justify-content-between">
<h3>评论</h3>
<div>
{{--检查账号是否登录,登录显示添加评论模块--}}
@if(Auth::check())
<button type="button" class="btn btn-primary" data-bs-toggle="modal"
data-bs-target="#staticBackdrop">
添加评论
</button>
@else
{{--没登录账号,跳转登录--}}
<a href="{{route('account.login')}}" class="btn btn-primary">添加评论</a>
@endif
</div>
</div>
@if($book->reviews->isNotEmpty())
@foreach($book->reviews as $review)
<div class="card border-0 shadow-lg my-4">
<div class="card-body">
<div class="d-flex justify-content-between">
<h4 class="mb-3">{{ $review->user->name }}</h4>
<span class="text-muted">
{{-- {{ \Carbon\carbon::parse($review->created_at)->format('d M,Y') }}--}}
{{ $review->created_at->diffForHumans() }}
</span>
</div>
@php
$ratingPer =($review->rating/5)*100
@endphp
<div class="mb-3">
<div class="star-rating d-inline-flex" title="">
<div class="star-rating d-inline-flex " title="">
<div class="back-stars ">
<i class="fa fa-star " aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<div class="front-stars" style="width: {{$ratingPer}}%">
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
</div>
</div>
</div>
</div>
</div>
<div class="content">
<p>{{ $review->review }}</p>
</div>
</div>
</div>
</div>
@endforeach
@endif
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal fade " id="staticBackdrop" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1"
aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="staticBackdropLabel">发表您的<strong>宝贵评论</strong>
</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="" id="bookReviewForm" name="bookReviewForm">
<input type="hidden" name="book_id" value="{{ $book->id }}">
<div class="modal-body">
<div class="mb-3">
<label for="" class="form-label">评论</label>
<textarea name="review" id="review" class="form-control" cols="5" rows="5"
placeholder="抢首评,友善交流"></textarea>
<p class="invalid-feedback" id="review-error"></p>
</div>
<div class="mb-3">
<label for="" class="form-label">评分</label>
<select name="rating" id="rating" class="form-control">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="submit" class="btn btn-primary">发表</button>
</div>
</form>
</div>
</div>
</div>
@endsection
{{--ajax发布评论--}}
@section('script')
<script>
$("#bookReviewForm").submit(function (e) {
e.preventDefault();
$.ajax({
url: '{{ route('book.saveReview') }}',
type: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token()}}'
},
data: $("#bookReviewForm").serializeArray(),
success: function (respone) {
if (respone.status == false) {
var errors = respone.errors;
if (errors.review) {
$("#review").addClass('is-invalid')
$("#review-error").html(errors.review);
} else {
$("#review").removeClass('is-invalid')
$("#review-error").html('');
}
} else {
window.location.href = '{{ route("book.detail", $book->id) }}'
}
}
});
})
</script>
@endsection
后台评论控制器
php artisan make:controller ReviewController
App/Http/Controllers/ReviewController.php
<?php
namespace App\Http\Controllers;
use App\Models\Review;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class ReviewController extends Controller
{
// 显示评论主页
public function index(Request $request)
{
$reviews = Review::with('book', 'user')->orderBy('created_at', 'desc');
if (!empty($request->keyword)) {
$reviews = $reviews->where('review', 'like', '%' . $request->keyword . '%');
}
$reviews = $reviews->paginate(2);
return view('account.reviews.list', compact('reviews'));
}
// 评论编辑显示页
public function edit($id)
{
$review = Review::findOrFail($id);
return view('account.reviews.edit', compact('review'));
}
// 执行评论编辑
public function updateReview(Request $request, $id)
{
$review = Review::findOrFail($id);
$validator = Validator::make($request->all(), [
'review' => 'required',
'status' => 'required',
]);
// 如果验证失败,我们将返回错误
if ($validator->fails()) {
return redirect()->route('account.reviews.edit', $id)->withErrors($validator)->withInput();
}
$review->review = $request->review;
$review->status = $request->status;
$review->save();
session()->flash('success', '编辑评论成功~');
return redirect()->route('account.reviews');
}
// 删除评论
public function deleteReview(Request $request)
{
$id = $request->id;
$review = Review::find($id);
// 如果该条评论为null,返回一个错误提醒
if ($review == null) {
session()->flash('error', '未找到评论');
return response()->json([
'status' => false
]);
} else {
$review->delete();
session()->flash('success', '已成功删除评论');
return response()->json([
'status' => false
]);
}
}
}
后端评论模版
resources/views/account/reviews/list.blade.php
@extends('layouts.app')
@section('title', '书籍评论')
@section('content')
<div class="container">
<div class="row my-5">
<div class="col-md-3">
<div class="card border-0 shadow-lg">
<div class="card-header text-white">
欢迎, {{ Auth::user()->name }}
</div>
<div class="card-body">
<div class="text-center mb-3">
@if(Auth::user()->image !="")
<img src="{{ asset('uploads/profile/thumb/' .Auth::user()->image) }}"
class="img-fluid rounded-circle" alt="">
@endif
</div>
<div class="h5 text-center">
<strong>{{ Auth::user()->name }}</strong>
<p class="h6 mt-2 text-muted">5 Reviews</p>
</div>
</div>
</div>
<div class="card border-0 shadow-lg mt-3">
<div class="card-header text-white">
菜单
</div>
<div class="card-body sidebar">
{{-- 菜单选项 --}}
@include('layouts.sidebar')
</div>
</div>
</div>
<div class="col-md-9">
{{-- 表单验证文件 --}}
@include('layouts.message')
<div class="card border-0 shadow">
<div class="card-header text-white">
书籍评论
</div>
<div class="card-body pb-0">
<div class="d-flex justify-content-end">
<form action="" method="get">
<div class="d-flex">
<input type="text" class="form-control" name="keyword"
value="{{ Request::get('keyword') }}" placeholder="请您输入搜索词">
<button type="submit" class="btn btn-primary">Search</button>
<a href="{{ route('account.reviews') }}" class="btn btn-secondary ms-2">重</a>
</div>
</form>
</div>
<table class="table table-striped mt-3">
<thead class="table-dark">
<tr>
<th>评论</th>
<th>书籍</th>
<th>评分</th>
<th>发布时间</th>
<th>状态</th>
<th width="100">操作</th>
</tr>
<tbody>
{{--将空字符进行判断,有空字符也会判断为不空--}}
@if($reviews->isNotEmpty())
@foreach($reviews as $review)
<tr>
<td>{{ $review->review }}<br/><strong>账号:{{ $review->user->name }}</strong>
</td>
<td>{{ $review->book->title }}</td>
<td>{{ $review->rating }}</td>
<td>
{{ $review->created_at->diffForHumans() }}
</td>
<td>
@if($review->status ==1 )
<span class="text-success">开启</span>
@else
<span class="text-danger">关闭</span>
@endif
</td>
<td>
<a href="{{ route('account.reviews.edit',$review->id) }}" class="btn btn-primary btn-sm"><i
class="fa-regular fa-pen-to-square"></i>
</a>
<a href="#" onclick="deleteReview({{$review->id}})" class="btn btn-danger btn-sm"><i class="fa-solid fa-trash"></i></a>
</td>
</tr>
@endforeach
@endif
</tbody>
</thead>
</table>
@if($reviews->isNotEmpty())
{{--分页--}}
{{$reviews->links()}}
@endif
</div>
</div>
</div>
</div>
</div>
@endsection
{{--ajax删除书籍--}}
@section('script')
<script>
function deleteReview(id){
if (confirm("您确定要删除评论么")) {
$.ajax({
url: '{{ route("account.reviews.deleteReview") }}',
type: 'post',
data: {id: id},
headers: {
'X-CSRF-TOKEN': '{{ csrf_token()}}'
},
success: function (response) {
window.location.href = '{{ route("account.reviews") }}';
}
});
}
}
</script>
@endsection
resources/views/account/reviews/edit.blade.php
@extends('layouts.app')
@section('title', '编辑评论')
@section('content')
<div class="container">
<div class="row my-5">
<div class="col-md-3">
<div class="card border-0 shadow-lg">
<div class="card-header text-white">
欢迎, {{ Auth::user()->name }}
</div>
<div class="card-body">
<div class="text-center mb-3">
@if(Auth::user()->image !="")
<img src="{{ asset('uploads/profile/thumb/' .Auth::user()->image) }}"
class="img-fluid rounded-circle" alt="">
@endif
</div>
<div class="h5 text-center">
<strong>{{ Auth::user()->name }}</strong>
<p class="h6 mt-2 text-muted">5 Reviews</p>
</div>
</div>
</div>
<div class="card border-0 shadow-lg mt-3">
<div class="card-header text-white">
菜单
</div>
<div class="card-body sidebar">
{{-- 菜单选项 --}}
@include('layouts.sidebar')
</div>
</div>
</div>
<div class="col-md-9">
{{-- 表单验证文件 --}}
@include('layouts.message')
<div class="card border-0 shadow">
<div class="card-header text-white">
编辑评论
</div>
<div class="card-body pb-0">
<form action="{{ route('account.reviews.updateReview',$review->id) }}" method="post">
@csrf
<div class="mb-3">
<label for="name" class="form-label">评论</label>
<textarea class="form-control @error('review') is-invalid @enderror" name="review"
id="review">{{ old('review',$review->review) }}</textarea>
@error('review')
<p class="invalid-feedback">{{ $message }}</p>
@enderror
</div>
<div class="mb-3">
<label for="name" class="form-label">状态</label>
<select name="status" id="status" class="form-control @error('status') is-invalid @enderror">
<option value="1" {{ ($review->status == 1)? 'selected' : '' }}>开启</option>
<option value="0" {{ ($review->status == 0)? 'selected' : '' }}>关闭</option>
</select>
@error('status')
<p class="invalid-feedback">{{ $message }}</p>
@enderror
</div>
<button class="btn btn-primary mt-2">更新</button>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection