add image upload functionality with history tracking, implement processing job, and integrate Tailwind CSS

main
Artem-Darius Weber 1 month ago
parent 35d3d609a6
commit 2225751bc8

@ -0,0 +1,15 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\ProcessHistories;
class HistoryController extends Controller
{
public function index()
{
$histories = ProcessHistories::orderBy('id','desc')->get();
return view('history.index', compact('histories'));
}
}

@ -0,0 +1,95 @@
<?php
namespace App\Http\Livewire;
use Livewire\Component;
use Livewire\WithFileUploads;
use App\Models\ProcessHistories;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Storage;
use App\Jobs\ProcessImagesJob;
class ImageUpload extends Component
{
use WithFileUploads;
public $images = []; // Массив из 3 элементов
public $imagePreviews = [];
public $historyId;
public $status = 'idle';
// Возможные статусы: 'idle', 'uploaded', 'processing', 'done', ...
public $progressMessage = '';
public $queueJobId;
protected $rules = [
'images.*' => 'image|max:4096', // 4MB, можно изменить по желанию
];
public function updatedImages($value, $key)
{
// После обновления файла, генерируем превью
$this->validateOnly('images.*');
if ($this->images[$key]) {
$this->imagePreviews[$key] = $this->images[$key]->temporaryUrl();
}
}
public function startProcessing()
{
$this->validate();
// Сохраняем изображения
$savedImages = [];
foreach ($this->images as $uploaded) {
$path = $uploaded->store('images', 'public');
$savedImages[] = [
'path' => $path,
'original_name' => $uploaded->getClientOriginalName()
];
}
// Создаём запись в истории
$history = ProcessHistories::create([
'images' => $savedImages,
'started_at' => now(),
]);
$this->historyId = $history->id;
$this->status = 'processing';
$this->progressMessage = 'Ставим в очередь на обработку...';
// Отправляем задачу в очередь
$job = new ProcessImagesJob($history->id);
$dispatch = dispatch($job);
// Можно при желании сохранить ID задачи, если нужно
// $this->queueJobId = ... (в зависимости от драйвера очереди)
// Дальше фронт будет периодически обновлять состояние либо
// мы можем слушать события Livewire (Broadcasting) по готовности
}
public function pollStatus()
{
if ($this->historyId) {
$history = ProcessHistories::find($this->historyId);
if ($history) {
if ($history->finished_at) {
$this->status = 'done';
$this->progressMessage = 'Обработка завершена. Файл: ' . $history->step_file_name;
} else {
// Можно отобразить прогресс, если в историях где-то хранится этап
$count = $history->processed_count;
$this->progressMessage = "Обработка в процессе... {$count}/3 изображений";
}
}
}
}
public function render()
{
return view('livewire.image-upload');
}
}

@ -0,0 +1,82 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\ProcessHistories;
class ProcessImagesJob implements ShouldQueue
{
use InteractsWithQueue, Queueable, SerializesModels;
public $historyId;
public $tries = 3;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($historyId)
{
$this->historyId = $historyId;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$history = ProcessHistories::find($this->historyId);
if (!$history) {
return;
}
$startQueue = now();
// Допустим тут время ожидания очереди (между startQueue и started_at)
$queueWaitTime = $startQueue->diffInSeconds($history->started_at);
// Эмуляция обработки
// 1. Загрузка файлов (у нас уже загружены локально, можно считать что это этап)
$uploadTime = rand(1,3); // псевдо
sleep($uploadTime);
// 2. Запрос к внешнему API для обработки каждого изображения
$processingStart = now();
$processedCount = 0;
foreach ($history->images as $index => $img) {
// Имитируем обращение к API
sleep(2); // эмуляция API задержки
$processedCount++;
$history->processed_count = $processedCount;
$history->save();
}
$processingTime = $processingStart->diffInSeconds(now());
// 3. Сохранение итогового step-файла
$savingStart = now();
// Имитируем сохранение файла (например API возвращает имя файла)
$stepFileName = 'step_' . time() . '.txt';
Storage::disk('public')->put($stepFileName, "Данные шага обработки.");
$savingTime = $savingStart->diffInSeconds(now());
$history->step_file_name = $stepFileName;
$history->finished_at = now();
$history->stages_timing = [
'queue_wait_time' => $queueWaitTime,
'upload_time' => $uploadTime,
'processing_time' => $processingTime,
'saving_time' => $savingTime
];
$history->save();
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,4 @@
{
"/js/app.js": "/js/app.js",
"/css/app.css": "/css/app.css"
}

@ -0,0 +1,46 @@
<x-app-layout>
<x-slot name="header">
История обработок
</x-slot>
<div class="p-4">
<h1 class="text-2xl font-bold mb-4">История обработок</h1>
<table class="min-w-full border-collapse">
<thead>
<tr class="border-b">
<th class="p-2 text-left">ID</th>
<th class="p-2 text-left">Изображения</th>
<th class="p-2 text-left">Step файл</th>
<th class="p-2 text-left">Время начала</th>
<th class="p-2 text-left">Время окончания</th>
<th class="p-2 text-left">Обработано</th>
<th class="p-2 text-left">Затраты времени (сек)</th>
</tr>
</thead>
<tbody>
@foreach($histories as $h)
<tr class="border-b">
<td class="p-2">{{ $h->id }}</td>
<td class="p-2">
@foreach($h->images as $img)
<div>{{ $img['original_name'] }} ({{ $img['path'] }})</div>
@endforeach
</td>
<td class="p-2">{{ $h->step_file_name }}</td>
<td class="p-2">{{ $h->started_at }}</td>
<td class="p-2">{{ $h->finished_at }}</td>
<td class="p-2">{{ $h->processed_count }}/3</td>
<td class="p-2">
@if($h->stages_timing)
Очередь: {{ $h->stages_timing['queue_wait_time'] ?? 'N/A' }}<br>
Загрузка: {{ $h->stages_timing['upload_time'] ?? 'N/A' }}<br>
Обработка: {{ $h->stages_timing['processing_time'] ?? 'N/A' }}<br>
Сохранение: {{ $h->stages_timing['saving_time'] ?? 'N/A' }}<br>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</x-app-layout>

@ -13,6 +13,8 @@
<!-- Styles -->
@livewireStyles
<script src="https://cdn.tailwindcss.com"></script>
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>

@ -10,6 +10,8 @@
<!-- Fonts -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap">
<script src="https://cdn.tailwindcss.com"></script>
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>

@ -0,0 +1,36 @@
<div class="p-4 space-y-4">
<h1 class="text-2xl font-bold">Загрузка изображений</h1>
<div class="flex space-x-4">
@for($i=0; $i<3; $i++)
<div class="border-2 border-dashed border-gray-300 w-32 h-32 flex items-center justify-center">
@if(isset($imagePreviews[$i]))
<img src="{{ $imagePreviews[$i] }}" class="object-cover w-full h-full" alt="Preview">
@else
<label class="cursor-pointer text-center">
<span class="text-gray-500">Перетащите файл сюда или кликните</span>
<input type="file" class="hidden" wire:model="images.{{ $i }}">
</label>
@endif
</div>
@endfor
</div>
<div class="space-x-2">
<button wire:click="startProcessing"
class="bg-blue-500 text-white px-4 py-2 rounded"
@disabled($status !== 'idle' && $status !== 'uploaded')>
Начать
</button>
@if($status === 'processing')
<button wire:poll.2s="pollStatus" class="hidden"></button>
@endif
</div>
@if($progressMessage)
<div class="text-gray-700 mt-4">{{ $progressMessage }}</div>
@endif
@if($status === 'done')
<a href="{{ route('history') }}" class="text-blue-500 underline">Посмотреть историю обработок</a>
@endif
</div>

@ -26,3 +26,9 @@ Route::middleware([
return view('dashboard');
})->name('dashboard');
});
use App\Http\Controllers\HistoryController;
use App\Http\Livewire\ImageUpload;
Route::get('/upload', ImageUpload::class)->name('upload');
Route::get('/history', [HistoryController::class, 'index'])->name('history');
Loading…
Cancel
Save