使用微风构建带有图像上传的Laravel 10 Crud应用程序 - 逐步教程与repo示例
Laravel是一个流行的开源PHP框架,可轻松构建Web应用程序。它为Web开发人员提供了一组强大的工具和功能,例如路由,中间件,数据库集成和模板。
Laravel 10是Laravel的最新版本,于2023年发行。它具有几个新功能和改进,例如对PHP 8.1的支持,增强的安全功能和性能优化。
在本教程中,我们将使用最流行的PHP框架之一Laravel创建一个简单的CRUD(创建,阅读,更新,删除)应用程序。 CRUD应用程序将使我们能够管理博客文章列表,包括创建新帖子,阅读现有帖子,更新帖子和删除帖子。除此之外,我们还将学习如何上传和管理博客文章的图像。
在本教程结束时,您将对如何在Laravel中构建基本的CRUD应用程序以及如何与应用程序中的图像一起工作有很好的了解。让我们开始!
安装Laravel 10
在开始之前,请确保您在系统上安装了最新版本的PHP(至少PHP 8.1)和作曲家。要安装Laravel 10,您可以使用以下命令:
composer create-project --prefer-dist laravel/laravel:^10 pmid-laravel10-breeze
此命令将在名为pmid-laravel10-breeze的目录中创建一个新的Laravel 10项目。
初始化数据库
接下来,我们需要设置一个数据库。对于本教程,创建一个名为pmid_laravel10_breeze
的数据库。创建数据库后,我们需要在Laravel应用程序的.env
文件中修改数据库连接设置,以匹配新的数据库名称以及用户名和密码。
安装Laravel微风
Laravel Breeze是Laravel 8及更高版本的轻量级身份验证脚手架。它提供了一种简单的方法,可以在Laravel应用程序中添加身份验证功能。
要安装Laravel Breeze,在您的终端中运行以下命令:
composer require laravel/breeze --dev
通过作曲家下载Laravel Breeze后,我们可以运行breeze:install
Artisan命令在我们的Laravel 10应用程序中安装Laravel Breeze:
php artisan breeze:install
将出现一个提示,询问我们要安装哪个堆栈。选择“刀片”。可能会出现另一个提示,询问我们是否要启用暗模式支持。目前,我们不会使用它,所以回答“不”。最后,可能会出现提示,询问我们是否要使用害虫测试框架。目前,我们不会使用它,所以回答“否”。
创建帖子迁移
现在我们已经安装了Laravel 10和Laravel Breeze,让我们为posts
表创建一个迁移。该表将存储我们的CRUD教程的帖子数据。
要创建迁移,请在终端中运行以下命令:
php artisan make:migration "create posts table"
此命令将在database/migrations
目录中创建一个新的迁移文件。
打开迁移文件并添加以下代码以创建posts
表:
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->text('featured_image');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('posts');
}
此代码将使用id
,title
,content
,featured_image
和时间戳列创建一个邮票表。
迁移迁移
最后,让我们运行迁移以在数据库中创建posts
表。为此,请在终端中运行以下命令:
php artisan migrate
此命令将运行database/migrations
目录中的所有未出色迁移。
创建路线
现在我们已经设置了posts
表和数据库,让我们创建一些路由来处理博客文章的CRUD操作。
打开routes/web.php
文件并使用以下代码进行更新:
<?php
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
// Add the following route to the existing routes because we want the posts route accessible to authenticated users only.
// We'll use a resource route because it contains all the exact routes we need for a typical CRUD application.
Route::resource('posts', PostController::class);
});
require __DIR__.'/auth.php';
这将发布资源路由在现有路由中添加了,并应用了验证中间件以限制对身份验证的用户的访问。 PostController
作为资源控制器设置为处理所有典型的CRUD操作。
另请阅读:
创建PostController
现在,让我们创建一个后控制器来处理我们的博客文章的CRUD操作。要使用资源方法创建新的控制器,请在终端中运行以下命令:
php artisan make:controller PostController --resource
此命令将创建一个新的PostController
,其中包含app/Http/Controllers
目录中的所有资源方法。
打开PostController
并将以下代码添加到类:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
// Use the Post Model
use App\Models\Post;
// We will use Form Request to validate incoming requests from our store and update method
use App\Http\Requests\Post\StoreRequest;
use App\Http\Requests\Post\UpdateRequest;
class PostController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(): Response
{
return response()->view('posts.index', [
'posts' => Post::orderBy('updated_at', 'desc')->get(),
]);
}
/**
* Show the form for creating a new resource.
*/
public function create(): Response
{
return response()->view('posts.form');
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreRequest $request): RedirectResponse
{
$validated = $request->validated();
if ($request->hasFile('featured_image')) {
// put image in the public storage
$file = Storage::disk('public')->put('images/posts/featured-images', request()->file('featured_image'), 'public');
// get the image path in the url
$path = Storage::url($file);
$validated['featured_image'] = $path;
}
// insert only requests that already validated in the StoreRequest
$create = Post::create($validated);
if($create) {
// add flash for the success notification
session()->flash('notif.success', 'Post created successfully!');
return redirect()->route('posts.index');
}
return abort(500);
}
/**
* Display the specified resource.
*/
public function show(string $id): Response
{
return response()->view('posts.show', [
'post' => Post::findOrFail($id),
]);
}
/**
* Show the form for editing the specified resource.
*/
public function edit(string $id): Response
{
return response()->view('posts.form', [
'post' => Post::findOrFail($id),
]);
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateRequest $request, string $id): RedirectResponse
{
$post = Post::findOrFail($id);
$validated = $request->validated();
if ($request->hasFile('featured_image')) {
// get current image path and replace the storage path with public path
$currentImage = str_replace('/storage', '/public', $post->featured_image);
// delete current image
Storage::delete($currentImage);
$file = Storage::disk('public')->put('images/posts/featured-images', request()->file('featured_image'), 'public');
$path = Storage::url($file);
$validated['featured_image'] = $path;
}
$update = $post->update($validated);
if($update) {
session()->flash('notif.success', 'Post updated successfully!');
return redirect()->route('posts.index');
}
return abort(500);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id): RedirectResponse
{
$post = Post::findOrFail($id);
$currentImage = str_replace('/storage', '/public', $post->featured_image);
Storage::delete($currentImage);
$delete = $post->delete($id);
if($delete) {
session()->flash('notif.success', 'Post deleted successfully!');
return redirect()->route('posts.index');
}
return abort(500);
}
}
创建对Store()方法的表单请求
我们可以运行以下命令来为store()
创建表单请求:
php artisan make:request Post/StoreRequest
,然后将以下代码添加到类:
public function authorize(): bool
{
// dont' forget to set this as true
return true;
}
public function rules(): array
{
// make all of the fields required, set featured image to accept only images
return [
'title' => 'required|string|min:3|max:250',
'content' => 'required|string|min:3|max:6000',
'featured_image' => 'required|image|max:1024|mimes:jpg,jpeg,png',
];
}
创建更新()方法的表单请求
我们可以运行以下命令来为update()
创建表单请求:
php artisan make:request Post/UpdateRequest
此类的代码类似于StoreRequest
的代码,但featured_image
规则设置为无效:
public function authorize(): bool
{
// dont' forget to set this as true
return true;
}
public function rules(): array
{
// make all of the fields required, set featured image to accept only images
return [
'title' => 'required|string|min:3|max:250',
'content' => 'required|string|min:3|max:6000',
'featured_image' => 'nullable|image|max:1024|mimes:jpg,jpeg,png',
];
}
添加$填充到帖子模型
在app/Models/Post.php
文件中,添加可填充属性以指定可以质量分配的字段。这是一个例子:
class Post extends Model
{
protected $fillable = [
'title',
'content',
'featured_image',
];
}
为我们的公共存储创建符号链接
要使可以从Web访问的公共磁盘中存储文件,需要从public/storage
文件夹创建一个符号链接到storage/app/public
文件夹。默认情况下,公共磁盘使用本地驱动程序。我们的公共存储配置不需要更改。要创建符号链接,请运行以下命令:
php artisan storage:link
tailwindcss和Vite配置
我们将使用tailwindcss进行样式和Vite作为我们的资产捆绑助手。因此,我们需要稍微调整它们的配置。
由于我们将为应用程序编写其他样式,因此我们需要在编码视图时启动尾风CLI构建过程。为此,运行以下命令:
npx tailwindcss -i ./resources/css/app.css -o ./resources/css/main.css --watch
此命令将在resources/css/main.css
上生成一个新文件。现在,我们需要告诉Vite我们将使用main.css
而不是app.css
。
我们将重复使用resources/views/layouts/app.blade.php
布局,因此在“头部”部分中将VITE代码更新到以下:
{{-- some other code --}}
@vite(['resources/css/main.css', 'resources/js/app.js'])
</head>
设置布局
我们将更新布局主体内部的标题部分,以便可以处理成功消息的闪存通知。请使用以下代码进行更新:
<!-- Page Heading -->
@if (isset($header))
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{{ $header }}
{{-- check if there is a notif.success flash session --}}
@if (Session::has('notif.success'))
<div class="bg-blue-300 mt-2 p-4">
{{-- if it's there then print the notification --}}
<span class="text-white">{{ Session::get('notif.success') }}</span>
</div>
@endif
</div>
</header>
@endif
因此,我们的资源\ views \ layouts \ app.blade.php文件将使用以下代码更新:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
@vite(['resources/css/main.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100">
@include('layouts.navigation')
<!-- Page Heading -->
@if (isset($header))
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{{ $header }}
@if (Session::has('notif.success'))
<div class="bg-blue-300 mt-2 p-4">
<span class="text-white">{{ Session::get('notif.success') }}</span>
</div>
@endif
</div>
</header>
@endif
<!-- Page Content -->
<main>
{{ $slot }}
</main>
</div>
</body>
</html>
另请阅读:
创建索引视图
创建一个带有以下内容的新文件resources/views/posts/index.blade.php
:
{{-- use AppLayout Component located in app\View\Components\AppLayout.php which use resources\views\layouts\app.blade.php view --}}
<x-app-layout>
<x-slot name="header">
<div class="flex justify-between">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ 'Posts' }}
</h2>
<a href="{{ route('posts.create') }}" class="bg-blue-500 text-white px-4 py-2 rounded-md">ADD</a>
</div>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<table class="border-collapse table-auto w-full text-sm">
<thead>
<tr>
<th class="border-b font-medium p-4 pl-8 pt-0 pb-3 text-slate-400 text-left">Title</th>
<th class="border-b font-medium p-4 pl-8 pt-0 pb-3 text-slate-400 text-left">Created At</th>
<th class="border-b font-medium p-4 pl-8 pt-0 pb-3 text-slate-400 text-left">Updated At</th>
<th class="border-b font-medium p-4 pl-8 pt-0 pb-3 text-slate-400 text-left">Action</th>
</tr>
</thead>
<tbody class="bg-white">
{{-- populate our post data --}}
@foreach ($posts as $post)
<tr>
<td class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 text-slate-500 dark:text-slate-400">{{ $post->title }}</td>
<td class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 text-slate-500 dark:text-slate-400">{{ $post->created_at }}</td>
<td class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 text-slate-500 dark:text-slate-400">{{ $post->updated_at }}</td>
<td class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 text-slate-500 dark:text-slate-400">
<a href="{{ route('posts.show', $post->id) }}" class="border border-blue-500 hover:bg-blue-500 hover:text-white px-4 py-2 rounded-md">SHOW</a>
<a href="{{ route('posts.edit', $post->id) }}" class="border border-yellow-500 hover:bg-yellow-500 hover:text-white px-4 py-2 rounded-md">EDIT</a>
{{-- add delete button using form tag --}}
<form method="post" action="{{ route('posts.destroy', $post->id) }}" class="inline">
@csrf
@method('delete')
<button class="border border-red-500 hover:bg-red-500 hover:text-white px-4 py-2 rounded-md">DELETE</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</x-app-layout>
创建显示视图
创建一个带有以下内容的新文件resources/views/posts/show.blade.php
:
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ 'Show' }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<div class="mb-6">
<h2 class="text-lg font-medium text-gray-900">
{{ 'Title' }}
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ $post->title }}
</p>
</div>
<div class="mb-6">
<h2 class="text-lg font-medium text-gray-900">
{{ 'Content' }}
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ $post->content }}
</p>
</div>
<div class="mb-6">
<h2 class="text-lg font-medium text-gray-900">
{{ 'Featured Image' }}
</h2>
<p class="mt-1 text-sm text-gray-600">
<img class="h-64 w-128" src="{{ asset($post->featured_image) }}" alt="{{ $post->title }}" srcset="">
</p>
</div>
<div class="mb-6">
<h2 class="text-lg font-medium text-gray-900">
{{ 'Created At' }}
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ $post->created_at }}
</p>
</div>
<div class="mb-6">
<h2 class="text-lg font-medium text-gray-900">
{{ 'Updated At' }}
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ $post->updated_at }}
</p>
</div>
<a href="{{ route('posts.index') }}" class="bg-blue-500 text-white px-4 py-2 rounded-md">BACK</a>
</div>
</div>
</div>
</div>
</x-app-layout>
创建表单视图
创建一个新的文件resources/views/posts/form.blade.php
。在这里,我们将使用一些已经从Laravel Breeze获得的已经定义的组件。编写以下代码:
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{-- Use 'Edit' for edit mode and create for non-edit/create mode --}}
{{ isset($post) ? 'Edit' : 'Create' }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
{{-- don't forget to add multipart/form-data so we can accept file in our form --}}
<form method="post" action="{{ isset($post) ? route('posts.update', $post->id) : route('posts.store') }}" class="mt-6 space-y-6" enctype="multipart/form-data">class="mt-6 space-y-6">
@csrf
{{-- add @method('put') for edit mode --}}
@isset($post)
@method('put')
@endisset
<div>
<x-input-label for="title" value="Title" />
<x-text-input id="title" name="title" type="text" class="mt-1 block w-full" :value="$post->title ?? old('title')" required autofocus />
<x-input-error class="mt-2" :messages="$errors->get('title')" />
</div>
<div>
<x-input-label for="content" value="Content" />
{{-- use textarea-input component that we will create after this --}}
<x-textarea-input id="content" name="content" class="mt-1 block w-full" required autofocus>{{ $post->content ?? old('content') }}</x-textarea-input>
<x-input-error class="mt-2" :messages="$errors->get('content')" />
</div>
<div>
<x-input-label for="featured_image" value="Featured Image" />
<label class="block mt-2">
<span class="sr-only">Choose image</span>
<input type="file" id="featured_image" name="featured_image" class="block w-full text-sm text-slate-500
file:mr-4 file:py-2 file:px-4
file:rounded-full file:border-0
file:text-sm file:font-semibold
file:bg-violet-50 file:text-violet-700
hover:file:bg-violet-100
"/>
</label>
<div class="shrink-0 my-2">
<img id="featured_image_preview" class="h-64 w-128 object-cover rounded-md" src="{{ isset($post) ? asset($post->featured_image) : '' }}" alt="Featured image preview" />
</div>
<x-input-error class="mt-2" :messages="$errors->get('featured_image')" />
</div>
<div class="flex items-center gap-4">
<x-primary-button>{{ __('Save') }}</x-primary-button>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
// create onchange event listener for featured_image input
document.getElementById('featured_image').onchange = function(evt) {
const [file] = this.files
if (file) {
// if there is an image, create a preview in featured_image_preview
document.getElementById('featured_image_preview').src = URL.createObjectURL(file)
}
}
</script>
</x-app-layout>
创建Textarea输入组件
使用以下代码创建其他组件:
@props(['disabled' => false])
<textarea {{ $disabled ? 'disabled' : '' }} {!! $attributes->merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) !!}>{{$slot}}</textarea>
更新导航组件
使用以下代码更新导航组件:
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<a href="{{ route('dashboard') }}">
<x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
</a>
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-nav-link>
<!-- add this -->
<x-nav-link :href="route('posts.index')" :active="request()->routeIs('posts.index')">
{{'Posts' }}
</x-nav-link>
</div>
</div>
<!-- Settings Dropdown -->
<div class="hidden sm:flex sm:items-center sm:ml-6">
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
<div>{{ Auth::user()->name }}</div>
<div class="ml-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</div>
</button>
</x-slot>
<x-slot name="content">
<x-dropdown-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-dropdown-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-dropdown-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-dropdown-link>
</form>
</x-slot>
</x-dropdown>
</div>
<!-- Hamburger -->
<div class="-mr-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
<!-- Responsive Navigation Menu -->
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1">
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-responsive-nav-link>
<!-- add this -->
<x-responsive-nav-link :href="route('posts.index')" :active="request()->routeIs('posts.index')">
{{ 'Posts' }}
</x-responsive-nav-link>
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200">
<div class="px-4">
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
</div>
<div class="mt-3 space-y-1">
<x-responsive-nav-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-responsive-nav-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-responsive-nav-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-responsive-nav-link>
</form>
</div>
</div>
</div>
</nav>
在这里,我们添加帖子链接到桌面和移动版本导航。
测试我们的Laravel应用程序
自从安装Laravel Breeze以来还没有运行npm install
,请确保首先运行它以安装所有必要的依赖项。如果您已经这样做,可以跳过此步骤。接下来,运行npm run dev
开始使用Vite。
设置了前端依赖项后,您可以通过运行以下命令来运行本地开发服务器:
php artisan serve
另请阅读:
欢迎页面
您将看到以下Laravel 10欢迎页面。您还将在Navbar上看到登录和登记链接,因为我们已经安装了Laravel Breeze。
注册自己,然后转到仪表板页。
仪表板页面
这是仪表板页面的外观。您还应该在名为帖子的Navbar上看到一个附加链接:
帖子列表页面
这是/posts
中的帖子列表页面的样子。它应该具有 show ,编辑和 delete 按钮的表格:
显示帖子页面
这是显示帖子页面的外观:
创建和编辑帖子页面
这是图像预览的创建邮政页面应该看起来像。这也类似于编辑帖子页面:
通知显示
我们成功创建,编辑或删除记录后应出现通知:
结论
在本教程中,我们学会了如何使用Laravel 8和Laravel Breeze来创建一个简单的CRUD应用程序进行身份验证。我们涵盖了Laravel的基本概念,例如路由,控制器,视图和模型,以及使用Laravel Breeze的组件来处理用户身份验证。
我们还学会了如何使用尾风CSS和VITE来设计我们的应用程序并优化资产捆绑。此外,我们介绍了如何处理闪存通知并在我们的应用程序中上传。
总的来说,本教程为使用Laravel构建CRUD应用程序提供了坚实的基础,并且在获得的知识中,您可以轻松地扩展此应用程序以满足您的特定要求。
可以在fajarwz/blog-laravel10-crud-image找到此示例的存储库。