Skip to content
Laravel 技术公众号,欢迎关注

文章管理(Post)

介绍

本节将一步一步实现文章功能,包括:数据表迁移、模型、表单验证、控制器、资源路由、Blade 视图以及 Seeder。完成后你可以完成文章的新增、编辑、查看、删除、列表筛选搜索,以及“我的文章”列表。


创建迁移(Migration)

使用命令创建迁移文件:

shell
php artisan make:migration create_posts_table

该命令会在 database/migrations/ 下生成一个以时间戳为前缀的文件,例如:xxxx_xx_xx_xxxxxx_create_posts_table.php。 打开该文件,写入如下内容(主要看 up 方法):

php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title')->comment('文章标题');
            $table->text('content')->comment('文章内容');
            $table->foreignId('user_id')->constrained()->onDelete('cascade')->comment('作者ID');
            $table->foreignId('category_id')->nullable()->constrained()->onDelete('set null')->comment('分类ID');
            $table->enum('status', ['draft', 'published'])->default('draft')->comment('文章状态');
            $table->timestamps();

            // 索引优化
            $table->index(['user_id', 'status']);
            $table->index(['category_id', 'status']);
            $table->index('created_at');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

写好之后,使用下面的命令运行迁移:

shell
php artisan migrate

创建模型(Model)

使用命令创建模型文件:

shell
php artisan make:model Post

模型文件路径:app/Models/Post.php

然后写入下面的内容:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'content',
        'user_id',
        'category_id',
        'status',
    ];

    // 作者
    public function user()
    {
        return $this->belongsTo(User::class);
    }

    // 分类
    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    // 已发布作用域
    public function scopePublished($query)
    {
        return $query->where('status', 'published');
    }

    // 草稿作用域
    public function scopeDraft($query)
    {
        return $query->where('status', 'draft');
    }
}

表单验证(FormRequest)

使用命令创建请求验证类:

shell
php artisan make:request PostRequest

文件路径:app/Http/Requests/PostRequest.php

然后写入下面的内容:

php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class PostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'title' => 'required|max:255',
            'content' => 'required|min:10',
            'category_id' => 'nullable|exists:categories,id',
            'status' => 'required|in:draft,published',
        ];
    }

    public function messages(): array
    {
        return [
            'title.required' => '请输入文章标题',
            'title.max' => '文章标题不能超过255个字符',
            'content.required' => '请输入文章内容',
            'content.min' => '文章内容至少需要10个字符',
            'category_id.exists' => '选择的分类不存在',
            'status.required' => '请选择文章状态',
            'status.in' => '文章状态无效',
        ];
    }
}

资源路由(Routes)

编辑 routes/web.php,注册文章相关路由:

php
// 文章
// 列表公开访问
Route::get('/posts', [\\App\\Http\\Controllers\\PostController::class, 'index'])->name('posts.index');

// 需要登录的文章路由
Route::middleware('auth')->group(function () {
    Route::get('/posts/create', [\\App\\Http\\Controllers\\PostController::class, 'create'])->name('posts.create');
    Route::post('/posts', [\\App\\Http\\Controllers\\PostController::class, 'store'])->name('posts.store');
    Route::get('/posts/{post}', [\\App\\Http\\Controllers\\PostController::class, 'show'])->name('posts.show');
    Route::get('/posts/{post}/edit', [\\App\\Http\\Controllers\\PostController::class, 'edit'])->name('posts.edit');
    Route::put('/posts/{post}', [\\App\\Http\\Controllers\\PostController::class, 'update'])->name('posts.update');
    Route::delete('/posts/{post}', [\\App\\Http\\Controllers\\PostController::class, 'destroy'])->name('posts.destroy');
    Route::get('/my-posts', [\\App\\Http\\Controllers\\PostController::class, 'myPosts'])->name('posts.my-posts');
});

控制器(Controller)

使用命令创建控制器:

shell
php artisan make:controller PostController

控制器文件路径:app/Http/Controllers/PostController.php

然后写入下面的内容:

php
<?php

namespace App\Http\Controllers;

use App\Models\Post;
use App\Models\Category;
use App\Http\Requests\PostRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class PostController extends Controller
{
    // 中间件在 routes/web.php 中声明,控制器无需再声明

    // 列表(发布可见,支持分类筛选与标题搜索)
    public function index(Request $request)
    {
        $query = Post::with(['user', 'category'])->published();

        if ($request->has('category') && $request->category) {
            $query->where('category_id', $request->category);
        }

        if ($request->has('search') && $request->search) {
            $query->where('title', 'like', '%'.$request->search.'%');
        }

        $posts = $query->orderBy('created_at', 'desc')->paginate(10);
        $categories = Category::all();

        return view('posts.index', compact('posts', 'categories'));
    }

    // 创建表单
    public function create()
    {
        $categories = Category::all();
        return view('posts.create', compact('categories'));
    }

    // 保存文章
    public function store(PostRequest $request)
    {
        $post = new Post();
        $post->title = $request->title;
        $post->content = $request->content;
        $post->category_id = $request->category_id;
        $post->status = $request->status;
        $post->user_id = Auth::id();
        $post->save();

        return redirect()->route('posts.show', $post)->with('success', '文章创建成功!');
    }

    // 详情(草稿仅作者可见)
    public function show(Post $post)
    {
        if ($post->status === 'draft' && $post->user_id !== Auth::id()) {
            abort(404);
        }

        $post->load(['user', 'category']);
        return view('posts.show', compact('post'));
    }

    // 编辑表单(仅作者可编辑)
    public function edit(Post $post)
    {
        if ($post->user_id !== Auth::id()) {
            abort(403, '您没有权限编辑此文章');
        }

        $categories = Category::all();
        return view('posts.edit', compact('post', 'categories'));
    }

    // 更新文章(仅作者可更新)
    public function update(PostRequest $request, Post $post)
    {
        if ($post->user_id !== Auth::id()) {
            abort(403, '您没有权限编辑此文章');
        }

        $post->title = $request->title;
        $post->content = $request->content;
        $post->category_id = $request->category_id;
        $post->status = $request->status;
        $post->save();

        return redirect()->route('posts.show', $post)->with('success', '文章更新成功!');
    }

    // 删除文章(仅作者可删除)
    public function destroy(Post $post)
    {
        if ($post->user_id !== Auth::id()) {
            abort(403, '您没有权限删除此文章');
        }

        $post->delete();
        return redirect()->route('posts.index')->with('success', '文章删除成功!');
    }

    // 我的文章
    public function myPosts()
    {
        $posts = Auth::user()->posts()->with('category')->orderBy('created_at', 'desc')->paginate(10);
        return view('posts.my-posts', compact('posts'));
    }
}

视图(Blade Views)

列表页

使用命令创建视图文件:

shell
php artisan make:view posts/index

然后找到 resources/views/posts/index.blade.php, 写入下面的内容:

php
@extends('components.layout')

@section('content')
<div class="container px-4 py-8 mx-auto">
    <div class="flex justify-between items-center mb-8">
        <h1 class="text-3xl font-bold text-gray-900">文章列表</h1>
        @auth
        <a href="{{ route('posts.create') }}" class="px-4 py-2 font-bold text-white bg-blue-500 rounded hover:bg-blue-700">
            写文章
        </a>
        @endauth
    </div>

    <!-- 搜索和筛选 -->
    <div class="p-6 mb-8 bg-white rounded-lg shadow-md">
        <form method="GET" action="{{ route('posts.index') }}" class="flex gap-4">
            <div class="flex-1">
                <input type="text" name="search" value="{{ request('search') }}"
                       placeholder="搜索文章标题..."
                       class="px-3 py-2 w-full rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500">
            </div>
            <div class="w-48">
                <select name="category" class="px-3 py-2 w-full rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500">
                    <option value="">所有分类</option>
                    @foreach($categories as $category)
                        <option value="{{ $category->id }}" {{ request('category') == $category->id ? 'selected' : '' }}>
                            {{ $category->name }}
                        </option>
                    @endforeach
                </select>
            </div>
            <button type="submit" class="px-4 py-2 font-bold text-white bg-gray-500 rounded hover:bg-gray-700">
                搜索
            </button>
        </form>
    </div>

    <!-- 文章列表 -->
    <div class="space-y-6">
        @forelse($posts as $post)
        <article class="overflow-hidden bg-white rounded-lg shadow-md">
            <div class="p-6">
                <div class="flex justify-between items-center mb-4">
                    <div class="flex items-center space-x-4">
                        <img src="{{ $post->user->avatar }}" alt="{{ $post->user->name }}"
                             class="w-10 h-10 rounded-full">
                        <div>
                            <p class="text-sm text-gray-600">{{ $post->user->name }}</p>
                            <p class="text-xs text-gray-500">{{ $post->created_at->format('Y-m-d H:i') }}</p>
                        </div>
                    </div>
                    @if($post->category)
                    <span class="px-2.5 py-0.5 text-xs font-medium text-blue-800 bg-blue-100 rounded">
                        {{ $post->category->name }}
                    </span>
                    @endif
                </div>

                <h2 class="mb-2 text-xl font-semibold text-gray-900">
                    <a href="{{ route('posts.show', $post) }}" class="hover:text-blue-600">
                        {{ $post->title }}
                    </a>
                </h2>

                <p class="mb-4 text-gray-600">
                    {{ Str::limit($post->content, 200) }}
                </p>

                <div class="flex justify-between items-center">
                    <div class="flex items-center space-x-4 text-sm text-gray-500">
                        <span>状态: {{ $post->status === 'published' ? '已发布' : '草稿' }}</span>
                    </div>
                    <a href="{{ route('posts.show', $post) }}"
                       class="font-medium text-blue-600 hover:text-blue-800">
                        阅读全文
                    </a>
                </div>
            </div>
        </article>
        @empty
        <div class="py-12 text-center">
            <p class="text-lg text-gray-500">暂无文章</p>
            @auth
            <a href="{{ route('posts.create') }}" class="inline-block px-4 py-2 mt-4 font-bold text-white bg-blue-500 rounded hover:bg-blue-700">
                写第一篇文章
            </a>
            @endauth
        </div>
        @endforelse
    </div>

    <!-- 分页 -->
    @if($posts->hasPages())
    <div class="mt-8">
        {{ $posts->appends(request()->query())->links() }}
    </div>
    @endif
    </div>
@endsection

访问 posts 路由,会看到如图列表页面,支持分类选择与标题搜索。如下图所示 Laravel 入门教程-文章列表

新增文章

使用命令创建视图文件:

shell
php artisan make:view posts/create

然后找到 resources/views/posts/create.blade.php, 写入下面的内容:

php
@extends('components.layout')

@section('content')
<div class="container px-4 py-8 mx-auto">
    <div class="mx-auto max-w-4xl">
        <div class="flex justify-between items-center mb-8">
            <h1 class="text-3xl font-bold text-gray-900">写文章</h1>
            <a href="{{ route('posts.index') }}" class="text-blue-600 hover:text-blue-800">
 返回文章列表
            </a>
        </div>

        <div class="p-6 bg-white rounded-lg shadow-md">
            <form method="POST" action="{{ route('posts.store') }}">
                @csrf

                <div class="mb-6">
                    <label for="title" class="block mb-2 text-sm font-medium text-gray-700">
                        文章标题 <span class="text-red-500">*</span>
                    </label>
                    <input type="text" id="title" name="title" value="{{ old('title') }}"
                           class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 @error('title') border-red-500 @enderror"
                           placeholder="请输入文章标题">
                    @error('title')
                        <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
                    @enderror
                </div>

                <div class="mb-6">
                    <label for="category_id" class="block mb-2 text-sm font-medium text-gray-700">
                        分类
                    </label>
                    <select id="category_id" name="category_id"
                            class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 @error('category_id') border-red-500 @enderror">
                        <option value="">选择分类(可选)</option>
                        @foreach($categories as $category)
                            <option value="{{ $category->id }}" {{ old('category_id') == $category->id ? 'selected' : '' }}>
                                {{ $category->name }}
                            </option>
                        @endforeach
                    </select>
                    @error('category_id')
                        <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
                    @enderror
                </div>

                <div class="mb-6">
                    <label for="content" class="block mb-2 text-sm font-medium text-gray-700">
                        文章内容 <span class="text-red-500">*</span>
                    </label>
                    <textarea id="content" name="content" rows="12"
                              class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 @error('content') border-red-500 @enderror"
                              placeholder="请输入文章内容...">{{ old('content') }}</textarea>
                    @error('content')
                        <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
                    @enderror
                </div>

                <div class="mb-6">
                    <label for="status" class="block mb-2 text-sm font-medium text-gray-700">
                        发布状态 <span class="text-red-500">*</span>
                    </label>
                    <select id="status" name="status"
                            class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 @error('status') border-red-500 @enderror">
                        <option value="draft" {{ old('status') == 'draft' ? 'selected' : '' }}>保存为草稿</option>
                        <option value="published" {{ old('status') == 'published' ? 'selected' : '' }}>立即发布</option>
                    </select>
                    @error('status')
                        <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
                    @enderror
                </div>

                <div class="flex justify-between items-center">
                    <button type="submit" class="px-6 py-2 font-bold text-white bg-blue-500 rounded hover:bg-blue-700">
                        保存文章
                    </button>
                    <a href="{{ route('posts.index') }}" class="text-gray-600 hover:text-gray-800">
                        取消
                    </a>
                </div>
            </form>
        </div>
    </div>
}</div>
@endsection

访问 posts/create 路由,会看到如下页面,注意 posts/create 需要登陆 laravel入门教程-创建文章 点击保存文章后,提交成功后将跳转到文章详情页。会看到下面的页面 laravel入门教程-文章详情

WARNING

如果提示 login 找不到的话,回到 routes/web.php 找到 login 路由,做以下修改,添加 name 方法

php
Route::get('/login', [\App\Http\Controllers\LoginController::class, 'index'])->name('login');

INFO

文章详情继续往下看

详情页

使用命令创建视图文件:

shell
php artisan make:view posts/show

然后找到 resources/views/posts/show.blade.php, 写入下面的内容:

php
@extends('components.layout')

@section('content')
<div class="container px-4 py-8 mx-auto">
    <div class="mx-auto max-w-4xl">
        <div class="mb-6">
            <a href="{{ route('posts.index') }}" class="text-blue-600 hover:text-blue-800"> 返回文章列表</a>
        </div>

        <article class="overflow-hidden bg-white rounded-lg shadow-md">
            <div class="p-8">
                <header class="mb-8">
                    <div class="flex justify-between items-center mb-4">
                        <div class="flex items-center space-x-4">
                            <img src="{{ $post->user->avatar }}" alt="{{ $post->user->name }}" class="w-12 h-12 rounded-full">
                            <div>
                                <p class="text-lg font-medium text-gray-900">{{ $post->user->name }}</p>
                                <p class="text-sm text-gray-500">{{ $post->created_at->format('Y年m月d日 H:i') }}</p>
                            </div>
                        </div>
                        @if($post->category)
                        <span class="px-3 py-1 text-sm font-medium text-blue-800 bg-blue-100 rounded-full">
                            {{ $post->category->name }}
                        </span>
                        @endif
                    </div>

                    <h1 class="mb-4 text-3xl font-bold text-gray-900">{{ $post->title }}</h1>

                    <div class="flex items-center space-x-4 text-sm text-gray-500">
                        <span>状态: {{ $post->status === 'published' ? '已发布' : '草稿' }}</span>
                        <span>更新时间: {{ $post->updated_at->format('Y-m-d H:i') }}</span>
                    </div>
                </header>

                <div class="mb-8 max-w-none prose">
                    <div class="leading-relaxed text-gray-700 whitespace-pre-wrap">
                        {{ $post->content }}
                    </div>
                </div>

                @auth
                @if($post->user_id === auth()->id())
                <div class="flex items-center pt-6 space-x-4 border-t border-gray-200">
                    <a href="{{ route('posts.edit', $post) }}" class="px-4 py-2 font-bold text-white bg-blue-500 rounded hover:bg-blue-700">
                        编辑文章
                    </a>
                    <form method="POST" action="{{ route('posts.destroy', $post) }}" onsubmit="return confirm('确定要删除这篇文章吗?')" class="inline">
                        @csrf
                        @method('DELETE')
                        <button type="submit" class="px-4 py-2 font-bold text-white bg-red-500 rounded hover:bg-red-700">
                            删除文章
                        </button>
                    </form>
                </div>
                @endif
                @endauth
            </div>
        </article>

        @if($post->category)
        <div class="p-6 mt-8 bg-white rounded-lg shadow-md">
            <h3 class="mb-4 text-lg font-semibold text-gray-900">分类信息</h3>
            <div class="flex items-center space-x-4">
                <span class="px-3 py-1 text-sm font-medium text-blue-800 bg-blue-100 rounded-full">
                    {{ $post->category->name }}
                </span>
                @if($post->category->description)
                <p class="text-gray-600">{{ $post->category->description }}</p>
                @endif
            </div>
        </div>
        @endif
    </div>
</div>
@endsection

编辑页

使用命令创建视图文件:

shell
php artisan make:view posts/edit

然后找到 resources/views/posts/edit.blade.php, 写入下面的内容:

php
@extends('components.layout')

@section('content')
<div class="container px-4 py-8 mx-auto">
    <div class="mx-auto max-w-4xl">
        <div class="flex justify-between items-center mb-8">
            <h1 class="text-3xl font-bold text-gray-900">编辑文章</h1>
            <a href="{{ route('posts.show', $post) }}" class="text-blue-600 hover:text-blue-800">
 返回文章详情
            </a>
        </div>

        <div class="p-6 bg-white rounded-lg shadow-md">
            <form method="POST" action="{{ route('posts.update', $post) }}">
                @csrf
                @method('PUT')

                <div class="mb-6">
                    <label for="title" class="block mb-2 text-sm font-medium text-gray-700">文章标题 <span class="text-red-500">*</span></label>
                    <input type="text" id="title" name="title" value="{{ old('title', $post->title) }}" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 @error('title') border-red-500 @enderror" placeholder="请输入文章标题">
                    @error('title')
                        <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
                    @enderror
                </div>

                <div class="mb-6">
                    <label for="category_id" class="block mb-2 text-sm font-medium text-gray-700">分类</label>
                    <select id="category_id" name="category_id" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 @error('category_id') border-red-500 @enderror">
                        <option value="">选择分类(可选)</option>
                        @foreach($categories as $category)
                            <option value="{{ $category->id }}" {{ old('category_id', $post->category_id) == $category->id ? 'selected' : '' }}>
                                {{ $category->name }}
                            </option>
                        @endforeach
                    </select>
                    @error('category_id')
                        <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
                    @enderror
                </div>

                <div class="mb-6">
                    <label for="content" class="block mb-2 text-sm font-medium text-gray-700">文章内容 <span class="text-red-500">*</span></label>
                    <textarea id="content" name="content" rows="12" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 @error('content') border-red-500 @enderror" placeholder="请输入文章内容...">{{ old('content', $post->content) }}</textarea>
                    @error('content')
                        <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
                    @enderror
                </div>

                <div class="mb-6">
                    <label for="status" class="block mb-2 text-sm font-medium text-gray-700">发布状态 <span class="text-red-500">*</span></label>
                    <select id="status" name="status" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 @error('status') border-red-500 @enderror">
                        <option value="draft" {{ old('status', $post->status) == 'draft' ? 'selected' : '' }}>保存为草稿</option>
                        <option value="published" {{ old('status', $post->status) == 'published' ? 'selected' : '' }}>立即发布</option>
                    </select>
                    @error('status')
                        <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
                    @enderror
                </div>

                <div class="flex justify-between items-center">
                    <button type="submit" class="px-6 py-2 font-bold text-white bg-blue-500 rounded hover:bg-blue-700">更新文章</button>
                    <a href="{{ route('posts.show', $post) }}" class="text-gray-600 hover:text-gray-800">取消</a>
                </div>
            </form>
        </div>
    </div>
</div>
@endsection

我的文章

使用命令创建视图文件:

shell
php artisan make:view posts/my-posts

然后找到 resources/views/posts/my-posts.blade.php, 写入下面的内容:

php
@extends('components.layout')

@section('content')
<div class="container px-4 py-8 mx-auto">
    <div class="flex justify-between items-center mb-8">
        <h1 class="text-3xl font-bold text-gray-900">我的文章</h1>
        <a href="{{ route('posts.create') }}" class="px-4 py-2 font-bold text-white bg-blue-500 rounded hover:bg-blue-700">写文章</a>
    </div>

    <div class="space-y-6">
        @forelse($posts as $post)
        <article class="overflow-hidden bg-white rounded-lg shadow-md">
            <div class="p-6">
                <div class="flex justify-between items-center mb-4">
                    <div class="flex items-center space-x-4">
                        <span class="bg-{{ $post->status === 'published' ? 'green' : 'yellow' }}-100 text-{{ $post->status === 'published' ? 'green' : 'yellow' }}-800 text-xs font-medium px-2.5 py-0.5 rounded">
                            {{ $post->status === 'published' ? '已发布' : '草稿' }}
                        </span>
                        @if($post->category)
                        <span class="px-2.5 py-0.5 text-xs font-medium text-blue-800 bg-blue-100 rounded">{{ $post->category->name }}</span>
                        @endif
                    </div>
                    <div class="text-sm text-gray-500">{{ $post->created_at->format('Y-m-d H:i') }}</div>
                </div>

                <h2 class="mb-2 text-xl font-semibold text-gray-900">
                    <a href="{{ route('posts.show', $post) }}" class="hover:text-blue-600">{{ $post->title }}</a>
                </h2>

                <p class="mb-4 text-gray-600">{{ Str::limit($post->content, 200) }}</p>

                <div class="flex justify-between items-center">
                    <div class="flex items-center space-x-4">
                        <a href="{{ route('posts.show', $post) }}" class="font-medium text-blue-600 hover:text-blue-800">查看</a>
                        <a href="{{ route('posts.edit', $post) }}" class="font-medium text-green-600 hover:text-green-800">编辑</a>
                        <form method="POST" action="{{ route('posts.destroy', $post) }}" onsubmit="return confirm('确定要删除这篇文章吗?')" class="inline">
                            @csrf
                            @method('DELETE')
                            <button type="submit" class="font-medium text-red-600 hover:text-red-800">删除</button>
                        </form>
                    </div>
                </div>
            </div>
        </article>
        @empty
        <div class="py-12 text-center">
            <p class="text-lg text-gray-500">您还没有写过文章</p>
            <a href="{{ route('posts.create') }}" class="inline-block px-4 py-2 mt-4 font-bold text-white bg-blue-500 rounded hover:bg-blue-700">写第一篇文章</a>
        </div>
        @endforelse
    </div>

    @if($posts->hasPages())
    <div class="mt-8">{{ $posts->links() }}</div>
    @endif
</div>
@endsection

输入 my-posts 路由,你会看到如下图所示的页面 Laravel 入门教程-我得文章

数据填充(Seeder 可选)

为了快速准备演示数据,使用命令创建 Seeder:

shell
php artisan make:seeder PostSeeder

然后在 database/seeders/PostSeeder.php 写入如下逻辑(会随机关联已有用户与分类):

php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\Post;
use App\Models\User;
use App\Models\Category;

class PostSeeder extends Seeder
{
    public function run(): void
    {
        $users = User::all();
        $categories = Category::all();

        if ($users->isEmpty() || $categories->isEmpty()) {
            return;
        }

        $posts = [
            [
                'title' => 'Laravel 入门指南',
                'content' => '...(省略长文)',
                'category_id' => $categories->where('name', '学习笔记')->first()->id,
                'status' => 'published',
            ],
            // 其余示例略...
        ];

        foreach ($posts as $postData) {
            Post::create([
                'title' => $postData['title'],
                'content' => $postData['content'],
                'user_id' => $users->random()->id,
                'category_id' => $postData['category_id'],
                'status' => $postData['status'],
            ]);
        }
    }
}

执行:

shell
php artisan db:seed --class=PostSeeder

验收与演示

  1. 访问 /posts 查看文章列表,尝试搜索与分类筛选
  2. 登录后访问 /posts/create 创建文章,提交后跳转到详情页
  3. 在详情页点击“编辑文章”修改并保存
  4. 访问 /my-posts 查看本人文章,进行查看/编辑/删除

小结

本节完成了文章功能的从 0 到 1 实现:迁移、模型、请求验证、控制器、资源路由、视图与测试数据填充。要点:

  • 列表仅展示已发布文章,草稿仅作者本人可见
  • 支持标题搜索与按分类筛选
  • 权限控制:仅作者可编辑/删除
  • “我的文章”用于个人文章的集中管理