Laravel 11 书评网4--评论模块

106 阅读2分钟

路由

// 书籍详情页
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> &nbsp; <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