Feat: Генерация thumbnail для обложек курсов

 Intervention Image установлен
 generateThumbnail() - создаёт копию 400x300px с crop по центру
 deleteThumbnails() - удаляет оригинал и thumb
 Обновлены store, update, destroy методы
 View используют оригиналы для show, thumb для списка

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
mirivlad 2026-03-26 12:39:10 +08:00
parent 65cb891658
commit b3d1daeea6
5 changed files with 172 additions and 7 deletions

View File

@ -9,6 +9,8 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Intervention\Image\ImageManager;
use Intervention\Image\Drivers\Gd\Driver;
class CourseController extends Controller class CourseController extends Controller
{ {
@ -17,6 +19,30 @@ class CourseController extends Controller
$this->middleware('auth'); $this->middleware('auth');
} }
private function generateThumbnail($imagePath, $width = 400, $height = 300)
{
$manager = new ImageManager(new Driver());
$image = $manager->read($imagePath);
// Пропорциональное уменьшение с crop по центру
$image->cover($width, $height, 'center');
// Сохраняем как _thumb версию
$thumbPath = str_replace('.', '_thumb.', $imagePath);
$image->save(storage_path('app/public/' . $thumbPath));
return str_replace('.', '_thumb.', $imagePath);
}
private function deleteThumbnails($path)
{
if (!$path) return;
$thumbPath = str_replace('.', '_thumb.', $path);
Storage::disk('public')->delete($thumbPath);
Storage::disk('public')->delete($path);
}
public function index(Request $request) public function index(Request $request)
{ {
Gate::authorize('viewAny', Course::class); Gate::authorize('viewAny', Course::class);
@ -81,7 +107,8 @@ class CourseController extends Controller
$validated['has_certificate'] = $request->boolean('has_certificate'); $validated['has_certificate'] = $request->boolean('has_certificate');
if ($request->hasFile('thumbnail')) { if ($request->hasFile('thumbnail')) {
$validated['thumbnail'] = $request->file('thumbnail')->store('courses/thumbnails', 'public'); $path = $request->file('thumbnail')->store('courses/thumbnails', 'public');
$validated['thumbnail'] = $this->generateThumbnail($path, 400, 300);
} }
$course = Course::create($validated); $course = Course::create($validated);
@ -134,8 +161,9 @@ class CourseController extends Controller
$validated['has_certificate'] = $request->boolean('has_certificate'); $validated['has_certificate'] = $request->boolean('has_certificate');
if ($request->hasFile('thumbnail')) { if ($request->hasFile('thumbnail')) {
if ($course->thumbnail) Storage::disk('public')->delete($course->thumbnail); if ($course->thumbnail) $this->deleteThumbnails($course->thumbnail);
$validated['thumbnail'] = $request->file('thumbnail')->store('courses/thumbnails', 'public'); $path = $request->file('thumbnail')->store('courses/thumbnails', 'public');
$validated['thumbnail'] = $this->generateThumbnail($path, 400, 300);
} }
$course->update($validated); $course->update($validated);
@ -147,7 +175,7 @@ class CourseController extends Controller
{ {
Gate::authorize('delete', $course); Gate::authorize('delete', $course);
if ($course->thumbnail) Storage::disk('public')->delete($course->thumbnail); $this->deleteThumbnails($course->thumbnail);
$course->delete(); $course->delete();

View File

@ -7,6 +7,7 @@
"require": { "require": {
"php": "^8.3", "php": "^8.3",
"guzzlehttp/guzzle": "^7.9", "guzzlehttp/guzzle": "^7.9",
"intervention/image": "^3.11",
"laravel/framework": "dev-master as 13.0", "laravel/framework": "dev-master as 13.0",
"laravel/sanctum": "^4.0", "laravel/sanctum": "^4.0",
"laravel/tinker": "^2.10", "laravel/tinker": "^2.10",

138
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "54cdb3ddf3312af9d237737159ea9d71", "content-hash": "5e9268b759e635964ff26f5e09bc95bc",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@ -1002,6 +1002,142 @@
], ],
"time": "2025-02-03T10:55:03+00:00" "time": "2025-02-03T10:55:03+00:00"
}, },
{
"name": "intervention/gif",
"version": "4.2.1",
"source": {
"type": "git",
"url": "https://github.com/Intervention/gif.git",
"reference": "6addac2c68b4bc0e37d0d3ccedda57eb84729c49"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/gif/zipball/6addac2c68b4bc0e37d0d3ccedda57eb84729c49",
"reference": "6addac2c68b4bc0e37d0d3ccedda57eb84729c49",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^10.0 || ^11.0",
"slevomat/coding-standard": "~8.0",
"squizlabs/php_codesniffer": "^3.8"
},
"type": "library",
"autoload": {
"psr-4": {
"Intervention\\Gif\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Oliver Vogel",
"email": "oliver@intervention.io",
"homepage": "https://intervention.io/"
}
],
"description": "Native PHP GIF Encoder/Decoder",
"homepage": "https://github.com/intervention/gif",
"keywords": [
"animation",
"gd",
"gif",
"image"
],
"funding": [
{
"url": "https://paypal.me/interventionio",
"type": "custom"
},
{
"url": "https://github.com/Intervention",
"type": "github"
},
{
"url": "https://ko-fi.com/interventionphp",
"type": "ko_fi"
}
],
"time": "2025-01-05T10:52:39+00:00"
},
{
"name": "intervention/image",
"version": "3.11.1",
"source": {
"type": "git",
"url": "https://github.com/Intervention/image.git",
"reference": "0f87254688e480fbb521e2a1ac6c11c784ca41af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/image/zipball/0f87254688e480fbb521e2a1ac6c11c784ca41af",
"reference": "0f87254688e480fbb521e2a1ac6c11c784ca41af",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"intervention/gif": "^4.2",
"php": "^8.1"
},
"require-dev": {
"mockery/mockery": "^1.6",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^10.0 || ^11.0",
"slevomat/coding-standard": "~8.0",
"squizlabs/php_codesniffer": "^3.8"
},
"suggest": {
"ext-exif": "Recommended to be able to read EXIF data properly."
},
"type": "library",
"autoload": {
"psr-4": {
"Intervention\\Image\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Oliver Vogel",
"email": "oliver@intervention.io",
"homepage": "https://intervention.io/"
}
],
"description": "PHP image manipulation",
"homepage": "https://image.intervention.io/",
"keywords": [
"gd",
"image",
"imagick",
"resize",
"thumbnail",
"watermark"
],
"funding": [
{
"url": "https://paypal.me/interventionio",
"type": "custom"
},
{
"url": "https://github.com/Intervention",
"type": "github"
},
{
"url": "https://ko-fi.com/interventionphp",
"type": "ko_fi"
}
],
"time": "2025-02-01T07:28:26+00:00"
},
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "dev-master", "version": "dev-master",

View File

@ -43,7 +43,7 @@
<div class="col-md-4 mb-4"> <div class="col-md-4 mb-4">
<div class="card h-100 shadow-sm"> <div class="card h-100 shadow-sm">
@if($course->thumbnail) @if($course->thumbnail)
<img src="{{ asset('storage/' . $course->thumbnail) }}" class="card-img-top" alt="{{ $course->title }}" style="height:200px;object-fit:cover;"> <img src="{{ asset('storage/' . str_replace('_thumb.', '.', $course->thumbnail)) }}" class="card-img-top" alt="{{ $course->title }}" style="height:200px;object-fit:cover;">
@else @else
<div class="card-img-top bg-secondary d-flex align-items-center justify-content-center" style="height:200px;"><i class="bi bi-book text-white" style="font-size:4rem;"></i></div> <div class="card-img-top bg-secondary d-flex align-items-center justify-content-center" style="height:200px;"><i class="bi bi-book text-white" style="font-size:4rem;"></i></div>
@endif @endif

View File

@ -15,7 +15,7 @@
<div class="row"> <div class="row">
<div class="col-md-4 mb-4"> <div class="col-md-4 mb-4">
<div class="card shadow-sm"> <div class="card shadow-sm">
@if($course->thumbnail)<img src="{{ asset('storage/' . $course->thumbnail) }}" class="card-img-top" alt="{{ $course->title }}"> @if($course->thumbnail)<img src="{{ asset('storage/' . str_replace('_thumb.', '.', $course->thumbnail)) }}" class="card-img-top" alt="{{ $course->title }}">
@else<div class="card-img-top bg-secondary d-flex align-items-center justify-content-center" style="height:200px;"><i class="bi bi-book text-white" style="font-size:4rem;"></i></div>@endif @else<div class="card-img-top bg-secondary d-flex align-items-center justify-content-center" style="height:200px;"><i class="bi bi-book text-white" style="font-size:4rem;"></i></div>@endif
</div> </div>
</div> </div>