fix: resolve image upload issues in posts

- Dashboard: disable submit button during uploads, show upload errors, guard against pending uploads
- Filament: add maxParallelUploads(1) to fix race condition (filamentphp#13306)
- Filament: add image/webp to gallery acceptedFileTypes, increase resize 30->50
- Dashboard: add image/webp to gallery input accept
- Add missing notifications table migration for Filament databaseNotifications
This commit is contained in:
F4ilji
2026-09-18 02:25:36 +05:00
parent 1853b087f4
commit 4a776a42e2
3 changed files with 81 additions and 7 deletions
+4 -3
View File
@@ -160,7 +160,7 @@ class PostForm
->image() ->image()
->directory('posts/gallery') ->directory('posts/gallery')
->optimize('jpg') ->optimize('jpg')
->resize(30) ->resize(50)
->imageEditor() ->imageEditor()
->multiple() ->multiple()
->reorderable() ->reorderable()
@@ -168,8 +168,9 @@ class PostForm
->helperText('Загрузите дополнительные изображения для галереи') ->helperText('Загрузите дополнительные изображения для галереи')
->maxFiles(200) ->maxFiles(200)
->maxSize(20480) ->maxSize(20480)
->acceptedFileTypes(['image/jpeg', 'image/png']) ->acceptedFileTypes(['image/jpeg', 'image/png', 'image/webp'])
->imagePreviewHeight('150'), ->imagePreviewHeight('150')
->maxParallelUploads(1),
]), ]),
Tabs\Tab::make('Слайдер') Tabs\Tab::make('Слайдер')
->icon('heroicon-o-view-columns') ->icon('heroicon-o-view-columns')
@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('notifications', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('type');
$table->morphs('notifiable');
$table->text('data');
$table->timestamp('read_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('notifications');
}
};
+46 -4
View File
@@ -26,15 +26,15 @@
<button <button
type="button" type="button"
@click="submitForm" @click="submitForm"
:disabled="form.processing" :disabled="form.processing || uploadingImages"
class="inline-flex items-center gap-2 px-5 py-2 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary-hover transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow-md" class="inline-flex items-center gap-2 px-5 py-2 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary-hover transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow-md"
> >
<svg v-if="form.processing" class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24"> <svg v-if="form.processing || uploadingImages" class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg> </svg>
<DashboardIcon v-else name="check" size="4" /> <DashboardIcon v-else name="check" size="4" />
{{ isEdit ? 'Сохранить' : 'Создать' }} {{ uploadStatusText }}
</button> </button>
</div> </div>
</div> </div>
@@ -76,6 +76,28 @@
</div> </div>
</transition> </transition>
<!-- Upload Error -->
<transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="opacity-0 -translate-y-2"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition duration-200 ease-in"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 -translate-y-2"
>
<div v-if="uploadError" class="mb-4 p-4 bg-amber-500/10 border border-amber-500/20 rounded-lg">
<div class="flex items-start gap-3">
<DashboardIcon name="exclamation-triangle" size="5" class="text-amber-600 flex-shrink-0 mt-0.5" />
<div class="flex-1">
<span class="text-sm text-foreground font-medium">{{ uploadError }}</span>
</div>
<button @click="uploadError = null" class="text-muted-foreground-1 hover:text-foreground">
<DashboardIcon name="x-mark" size="4" />
</button>
</div>
</div>
</transition>
<form @submit.prevent="submitForm" class="space-y-6"> <form @submit.prevent="submitForm" class="space-y-6">
<!-- Tabs Navigation --> <!-- Tabs Navigation -->
<div class="bg-layer border border-layer-line rounded-lg shadow-xs overflow-hidden"> <div class="bg-layer border border-layer-line rounded-lg shadow-xs overflow-hidden">
@@ -497,7 +519,7 @@
ref="galleryInput" ref="galleryInput"
type="file" type="file"
class="hidden" class="hidden"
accept="image/jpeg,image/png" accept="image/jpeg,image/png,image/webp"
multiple multiple
@change="handleGalleryFileSelect" @change="handleGalleryFileSelect"
/> />
@@ -596,6 +618,7 @@ export default {
isDraggingPreview: false, isDraggingPreview: false,
isDraggingGallery: false, isDraggingGallery: false,
uploadingImages: false, uploadingImages: false,
uploadError: null,
newTag: '', newTag: '',
newAuthor: '', newAuthor: '',
PostStatus, PostStatus,
@@ -698,6 +721,11 @@ export default {
return null; return null;
}).filter(url => url !== null); }).filter(url => url !== null);
}, },
uploadStatusText() {
if (this.uploadingImages) return 'Загрузка файлов...';
return this.isEdit ? 'Сохранить' : 'Создать';
},
}, },
beforeUnmount() { beforeUnmount() {
@@ -812,6 +840,7 @@ export default {
if (!file) return; if (!file) return;
this.uploadingImages = true; this.uploadingImages = true;
this.uploadError = null;
try { try {
const formData = new FormData(); const formData = new FormData();
@@ -830,9 +859,12 @@ export default {
if (result.success && result.paths && result.paths.length > 0) { if (result.success && result.paths && result.paths.length > 0) {
this.form.preview = result.paths[0]; this.form.preview = result.paths[0];
} else {
this.uploadError = result.error || 'Не удалось загрузить главное изображение';
} }
} catch (error) { } catch (error) {
console.error('Preview upload error:', error); console.error('Preview upload error:', error);
this.uploadError = 'Ошибка сети при загрузке главного изображения. Проверьте подключение.';
} finally { } finally {
this.uploadingImages = false; this.uploadingImages = false;
} }
@@ -858,6 +890,7 @@ export default {
if (files.length === 0) return; if (files.length === 0) return;
this.uploadingImages = true; this.uploadingImages = true;
this.uploadError = null;
try { try {
const formData = new FormData(); const formData = new FormData();
@@ -880,9 +913,12 @@ export default {
result.paths.forEach(path => { result.paths.forEach(path => {
this.form.images.push(path); this.form.images.push(path);
}); });
} else {
this.uploadError = result.error || 'Не удалось загрузить изображения галереи';
} }
} catch (error) { } catch (error) {
console.error('Image upload error:', error); console.error('Image upload error:', error);
this.uploadError = 'Ошибка сети при загрузке изображений. Проверьте подключение.';
} finally { } finally {
this.uploadingImages = false; this.uploadingImages = false;
} }
@@ -951,8 +987,14 @@ export default {
}, },
submitForm() { submitForm() {
if (this.uploadingImages) {
this.uploadError = 'Дождитесь завершения загрузки изображений перед сохранением.';
return;
}
this.form.processing = true; this.form.processing = true;
this.form.errors = {}; this.form.errors = {};
this.uploadError = null;
// Фильтруем изображения - оставляем только строки (пути) // Фильтруем изображения - оставляем только строки (пути)
this.form.images = this.form.images.filter(img => typeof img === 'string' && img.length > 0); this.form.images = this.form.images.filter(img => typeof img === 'string' && img.length > 0);