Datei-Upload: Mehrfach-Upload mit Drag & Drop und Dateiliste
- Upload-Formular unterstützt jetzt mehrere Dateien gleichzeitig - Drag & Drop oder Klick zum Auswählen (mehrfach möglich) - Dateiliste mit Dateiname, Größe, individueller Kategorie-Auswahl und Entfernen-Button pro Datei - Standard-Kategorie kann oben gewählt werden, individuelle Kategorie pro Datei ist optional überschreibbar - Controller verarbeitet Array von Dateien (je max. 10 MB) - Übersetzungen in allen 6 Sprachen ergänzt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -52,31 +52,52 @@ class FileController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'file' => ['required', 'file', 'max:10240', 'mimes:pdf,docx,xlsx,jpg,jpeg,png,gif,webp'],
|
'files' => ['required', 'array', 'min:1'],
|
||||||
'file_category_id' => ['required', 'exists:file_categories,id'],
|
'files.*' => ['file', 'max:10240', 'mimes:pdf,docx,xlsx,jpg,jpeg,png,gif,webp'],
|
||||||
|
'categories' => ['required', 'array', 'min:1'],
|
||||||
|
'categories.*' => ['required', 'exists:file_categories,id'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$uploadedFile = $request->file('file');
|
$uploadedFiles = $request->file('files', []);
|
||||||
$extension = $uploadedFile->guessExtension();
|
$categories = $request->input('categories', []);
|
||||||
$storedName = Str::uuid() . '.' . $extension;
|
$count = 0;
|
||||||
|
|
||||||
Storage::disk('local')->putFileAs('files', $uploadedFile, $storedName);
|
foreach ($uploadedFiles as $index => $uploadedFile) {
|
||||||
|
if (!$uploadedFile || !$uploadedFile->isValid()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$file = new File([
|
$categoryId = $categories[$index] ?? null;
|
||||||
'file_category_id' => $request->file_category_id,
|
if (!$categoryId) {
|
||||||
'original_name' => $uploadedFile->getClientOriginalName(),
|
continue;
|
||||||
'mime_type' => $uploadedFile->getClientMimeType(),
|
}
|
||||||
'size' => $uploadedFile->getSize(),
|
|
||||||
]);
|
|
||||||
$file->stored_name = $storedName;
|
|
||||||
$file->disk = 'private';
|
|
||||||
$file->uploaded_by = auth()->id();
|
|
||||||
$file->save();
|
|
||||||
|
|
||||||
ActivityLog::logWithChanges('uploaded', __('admin.log_file_uploaded', ['name' => $file->original_name]), 'File', $file->id, null, ['name' => $file->original_name, 'category' => $file->category->name ?? '']);
|
$extension = $uploadedFile->guessExtension();
|
||||||
|
$storedName = Str::uuid() . '.' . $extension;
|
||||||
|
|
||||||
|
Storage::disk('local')->putFileAs('files', $uploadedFile, $storedName);
|
||||||
|
|
||||||
|
$file = new File([
|
||||||
|
'file_category_id' => $categoryId,
|
||||||
|
'original_name' => $uploadedFile->getClientOriginalName(),
|
||||||
|
'mime_type' => $uploadedFile->getClientMimeType(),
|
||||||
|
'size' => $uploadedFile->getSize(),
|
||||||
|
]);
|
||||||
|
$file->stored_name = $storedName;
|
||||||
|
$file->disk = 'private';
|
||||||
|
$file->uploaded_by = auth()->id();
|
||||||
|
$file->save();
|
||||||
|
|
||||||
|
ActivityLog::logWithChanges('uploaded', __('admin.log_file_uploaded', ['name' => $file->original_name]), 'File', $file->id, null, ['name' => $file->original_name, 'category' => $file->category->name ?? '']);
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = $count === 1
|
||||||
|
? __('admin.file_uploaded')
|
||||||
|
: __('admin.files_uploaded', ['count' => $count]);
|
||||||
|
|
||||||
return redirect()->route('admin.files.index')
|
return redirect()->route('admin.files.index')
|
||||||
->with('success', __('admin.file_uploaded'));
|
->with('success', $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function destroy(File $file): RedirectResponse
|
public function destroy(File $file): RedirectResponse
|
||||||
|
|||||||
@@ -194,6 +194,12 @@ return [
|
|||||||
'files_title' => 'إدارة الملفات',
|
'files_title' => 'إدارة الملفات',
|
||||||
'upload_file' => 'رفع ملف',
|
'upload_file' => 'رفع ملف',
|
||||||
'file_uploaded' => 'تم رفع الملف.',
|
'file_uploaded' => 'تم رفع الملف.',
|
||||||
|
'upload_files' => 'رفع الملفات',
|
||||||
|
'files_uploaded' => 'تم رفع :count ملفات.',
|
||||||
|
'select_files' => 'اختيار الملفات',
|
||||||
|
'drag_or_click_files' => 'اسحب الملفات هنا أو انقر للاختيار',
|
||||||
|
'files_selected' => 'ملف(ات) محددة',
|
||||||
|
'select_category_for_all' => 'يرجى اختيار فئة لجميع الملفات.',
|
||||||
'file_deleted' => 'تم حذف الملف.',
|
'file_deleted' => 'تم حذف الملف.',
|
||||||
'file_category' => 'الفئة',
|
'file_category' => 'الفئة',
|
||||||
'select_category' => 'اختر الفئة',
|
'select_category' => 'اختر الفئة',
|
||||||
|
|||||||
@@ -225,6 +225,12 @@ return [
|
|||||||
'files_title' => 'Dateiverwaltung',
|
'files_title' => 'Dateiverwaltung',
|
||||||
'upload_file' => 'Datei hochladen',
|
'upload_file' => 'Datei hochladen',
|
||||||
'file_uploaded' => 'Datei wurde hochgeladen.',
|
'file_uploaded' => 'Datei wurde hochgeladen.',
|
||||||
|
'upload_files' => 'Dateien hochladen',
|
||||||
|
'files_uploaded' => ':count Dateien wurden hochgeladen.',
|
||||||
|
'select_files' => 'Dateien auswählen',
|
||||||
|
'drag_or_click_files' => 'Dateien hierher ziehen oder klicken zum Auswählen',
|
||||||
|
'files_selected' => 'Datei(en) ausgewählt',
|
||||||
|
'select_category_for_all' => 'Bitte wählen Sie für alle Dateien eine Kategorie.',
|
||||||
'file_deleted' => 'Datei wurde gelöscht.',
|
'file_deleted' => 'Datei wurde gelöscht.',
|
||||||
'file_category' => 'Kategorie',
|
'file_category' => 'Kategorie',
|
||||||
'select_category' => 'Kategorie wählen',
|
'select_category' => 'Kategorie wählen',
|
||||||
|
|||||||
@@ -193,6 +193,12 @@ return [
|
|||||||
'files_title' => 'File Management',
|
'files_title' => 'File Management',
|
||||||
'upload_file' => 'Upload File',
|
'upload_file' => 'Upload File',
|
||||||
'file_uploaded' => 'File has been uploaded.',
|
'file_uploaded' => 'File has been uploaded.',
|
||||||
|
'upload_files' => 'Upload Files',
|
||||||
|
'files_uploaded' => ':count files have been uploaded.',
|
||||||
|
'select_files' => 'Select Files',
|
||||||
|
'drag_or_click_files' => 'Drag files here or click to select',
|
||||||
|
'files_selected' => 'file(s) selected',
|
||||||
|
'select_category_for_all' => 'Please select a category for all files.',
|
||||||
'file_deleted' => 'File has been deleted.',
|
'file_deleted' => 'File has been deleted.',
|
||||||
'file_category' => 'Category',
|
'file_category' => 'Category',
|
||||||
'select_category' => 'Select category',
|
'select_category' => 'Select category',
|
||||||
|
|||||||
@@ -194,6 +194,12 @@ return [
|
|||||||
'files_title' => 'Zarządzanie plikami',
|
'files_title' => 'Zarządzanie plikami',
|
||||||
'upload_file' => 'Prześlij plik',
|
'upload_file' => 'Prześlij plik',
|
||||||
'file_uploaded' => 'Plik został przesłany.',
|
'file_uploaded' => 'Plik został przesłany.',
|
||||||
|
'upload_files' => 'Prześlij pliki',
|
||||||
|
'files_uploaded' => 'Przesłano :count plików.',
|
||||||
|
'select_files' => 'Wybierz pliki',
|
||||||
|
'drag_or_click_files' => 'Przeciągnij pliki tutaj lub kliknij, aby wybrać',
|
||||||
|
'files_selected' => 'plik(ów) wybranych',
|
||||||
|
'select_category_for_all' => 'Proszę wybrać kategorię dla wszystkich plików.',
|
||||||
'file_deleted' => 'Plik został usunięty.',
|
'file_deleted' => 'Plik został usunięty.',
|
||||||
'file_category' => 'Kategoria',
|
'file_category' => 'Kategoria',
|
||||||
'select_category' => 'Wybierz kategorię',
|
'select_category' => 'Wybierz kategorię',
|
||||||
|
|||||||
@@ -212,6 +212,12 @@ return [
|
|||||||
'files_title' => 'Управление файлами',
|
'files_title' => 'Управление файлами',
|
||||||
'upload_file' => 'Загрузить файл',
|
'upload_file' => 'Загрузить файл',
|
||||||
'file_uploaded' => 'Файл был загружен.',
|
'file_uploaded' => 'Файл был загружен.',
|
||||||
|
'upload_files' => 'Загрузить файлы',
|
||||||
|
'files_uploaded' => 'Загружено файлов: :count.',
|
||||||
|
'select_files' => 'Выбрать файлы',
|
||||||
|
'drag_or_click_files' => 'Перетащите файлы сюда или нажмите для выбора',
|
||||||
|
'files_selected' => 'файл(ов) выбрано',
|
||||||
|
'select_category_for_all' => 'Пожалуйста, выберите категорию для всех файлов.',
|
||||||
'file_deleted' => 'Файл был удалён.',
|
'file_deleted' => 'Файл был удалён.',
|
||||||
'file_category' => 'Категория',
|
'file_category' => 'Категория',
|
||||||
'select_category' => 'Выберите категорию',
|
'select_category' => 'Выберите категорию',
|
||||||
|
|||||||
@@ -212,6 +212,12 @@ return [
|
|||||||
'files_title' => 'Dosya Yönetimi',
|
'files_title' => 'Dosya Yönetimi',
|
||||||
'upload_file' => 'Dosya Yükle',
|
'upload_file' => 'Dosya Yükle',
|
||||||
'file_uploaded' => 'Dosya yüklendi.',
|
'file_uploaded' => 'Dosya yüklendi.',
|
||||||
|
'upload_files' => 'Dosyaları Yükle',
|
||||||
|
'files_uploaded' => ':count dosya yüklendi.',
|
||||||
|
'select_files' => 'Dosyaları Seç',
|
||||||
|
'drag_or_click_files' => 'Dosyaları buraya sürükleyin veya seçmek için tıklayın',
|
||||||
|
'files_selected' => 'dosya seçildi',
|
||||||
|
'select_category_for_all' => 'Lütfen tüm dosyalar için bir kategori seçin.',
|
||||||
'file_deleted' => 'Dosya silindi.',
|
'file_deleted' => 'Dosya silindi.',
|
||||||
'file_category' => 'Kategori',
|
'file_category' => 'Kategori',
|
||||||
'select_category' => 'Kategori seçin',
|
'select_category' => 'Kategori seçin',
|
||||||
|
|||||||
@@ -1,47 +1,119 @@
|
|||||||
<x-layouts.admin :title="__('admin.upload_file')">
|
<x-layouts.admin :title="__('admin.upload_file')">
|
||||||
<h1 class="text-2xl font-bold mb-6">{{ __('admin.upload_file') }}</h1>
|
<h1 class="text-2xl font-bold mb-6">{{ __('admin.upload_file') }}</h1>
|
||||||
|
|
||||||
<div class="bg-white rounded-lg shadow p-6 max-w-lg">
|
<div class="bg-white rounded-lg shadow p-6 max-w-2xl">
|
||||||
<form method="POST" action="{{ route('admin.files.store') }}" enctype="multipart/form-data">
|
<form method="POST" action="{{ route('admin.files.store') }}" enctype="multipart/form-data"
|
||||||
|
x-data="{
|
||||||
|
files: [],
|
||||||
|
dragging: false,
|
||||||
|
defaultCategory: '',
|
||||||
|
addFiles(fileList) {
|
||||||
|
for (const f of fileList) {
|
||||||
|
if (f.size > 10485760) continue;
|
||||||
|
this.files.push({ file: f, name: f.name, size: f.size, category: this.defaultCategory });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeFile(index) {
|
||||||
|
this.files.splice(index, 1);
|
||||||
|
},
|
||||||
|
humanSize(bytes) {
|
||||||
|
if (bytes < 1024) return bytes + ' B';
|
||||||
|
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||||||
|
return (bytes / 1048576).toFixed(1) + ' MB';
|
||||||
|
}
|
||||||
|
}">
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
|
{{-- Standard-Kategorie --}}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="file_category_id" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.file_category') }} *</label>
|
<label for="default_category" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.file_category') }} *</label>
|
||||||
<select name="file_category_id" id="file_category_id" required class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
<select id="default_category" x-model="defaultCategory" class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||||
<option value="">{{ __('admin.select_category') }}</option>
|
<option value="">{{ __('admin.select_category') }}</option>
|
||||||
@foreach ($categories as $cat)
|
@foreach ($categories as $cat)
|
||||||
<option value="{{ $cat->id }}" {{ old('file_category_id') == $cat->id ? 'selected' : '' }}>{{ $cat->name }}</option>
|
<option value="{{ $cat->id }}">{{ $cat->name }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
@error('file_category_id')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- Drag & Drop Zone --}}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="file" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.upload_file') }} *</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.select_files') }}</label>
|
||||||
<div x-data="{ fileName: '', dragging: false }" class="relative">
|
<div
|
||||||
<div
|
@dragover.prevent="dragging = true"
|
||||||
@dragover.prevent="dragging = true"
|
@dragleave.prevent="dragging = false"
|
||||||
@dragleave.prevent="dragging = false"
|
@drop.prevent="dragging = false; addFiles($event.dataTransfer.files)"
|
||||||
@drop.prevent="dragging = false; $refs.fileInput.files = $event.dataTransfer.files; fileName = $refs.fileInput.files[0]?.name || ''"
|
:class="dragging ? 'border-blue-400 bg-blue-50' : 'border-gray-300'"
|
||||||
:class="dragging ? 'border-blue-400 bg-blue-50' : 'border-gray-300'"
|
class="border-2 border-dashed rounded-lg p-8 text-center cursor-pointer hover:border-blue-400 transition-colors"
|
||||||
class="border-2 border-dashed rounded-lg p-8 text-center cursor-pointer hover:border-blue-400 transition-colors"
|
@click="$refs.fileInput.click()">
|
||||||
@click="$refs.fileInput.click()">
|
<svg class="mx-auto h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<svg class="mx-auto h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
</svg>
|
||||||
</svg>
|
<p class="mt-2 text-sm text-gray-600" x-show="!files.length">{{ __('admin.drag_or_click_files') }}</p>
|
||||||
<p class="mt-2 text-sm text-gray-600" x-show="!fileName">{{ __('admin.allowed_file_types') }}</p>
|
<p class="mt-1 text-xs text-gray-500" x-show="!files.length">{{ __('admin.allowed_file_types') }} · {{ __('admin.max_file_size') }}</p>
|
||||||
<p class="mt-1 text-xs text-gray-500" x-show="!fileName">{{ __('admin.max_file_size') }}</p>
|
<p class="mt-2 text-sm font-medium text-blue-600" x-show="files.length" x-text="files.length + ' {{ __('admin.files_selected') }}'"></p>
|
||||||
<p class="mt-2 text-sm font-medium text-blue-600" x-show="fileName" x-text="fileName"></p>
|
|
||||||
</div>
|
|
||||||
<input type="file" name="file" id="file" x-ref="fileInput" class="hidden"
|
|
||||||
accept=".pdf,.docx,.xlsx,.jpg,.jpeg,.png,.gif,.webp"
|
|
||||||
@change="fileName = $refs.fileInput.files[0]?.name || ''">
|
|
||||||
</div>
|
</div>
|
||||||
@error('file')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
<input type="file" x-ref="fileInput" class="hidden" multiple
|
||||||
|
accept=".pdf,.docx,.xlsx,.jpg,.jpeg,.png,.gif,.webp"
|
||||||
|
@change="addFiles($refs.fileInput.files); $refs.fileInput.value = ''">
|
||||||
|
@error('files')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
|
@error('files.*')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
|
@error('categories')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
|
@error('categories.*')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- Datei-Liste --}}
|
||||||
|
<template x-if="files.length > 0">
|
||||||
|
<div class="mb-4 space-y-2">
|
||||||
|
<template x-for="(f, index) in files" :key="index">
|
||||||
|
<div class="flex items-center gap-3 bg-gray-50 rounded-md px-3 py-2">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm font-medium text-gray-800 truncate" x-text="f.name"></p>
|
||||||
|
<p class="text-xs text-gray-500" x-text="humanSize(f.size)"></p>
|
||||||
|
</div>
|
||||||
|
<select x-model="f.category" class="px-2 py-1 border border-gray-300 rounded-md text-sm shrink-0">
|
||||||
|
<option value="">{{ __('admin.select_category') }}</option>
|
||||||
|
@foreach ($categories as $cat)
|
||||||
|
<option value="{{ $cat->id }}">{{ $cat->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<button type="button" @click="removeFile(index)" class="text-red-500 hover:text-red-700 shrink-0">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
{{-- Hidden Inputs für tatsächlichen Upload --}}
|
||||||
|
<input type="file" name="files[]" x-ref="submitFiles" class="hidden" multiple>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 text-sm font-medium">{{ __('admin.upload_file') }}</button>
|
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 text-sm font-medium"
|
||||||
|
:disabled="!files.length"
|
||||||
|
:class="!files.length ? 'opacity-50 cursor-not-allowed' : ''"
|
||||||
|
@click.prevent="
|
||||||
|
const dt = new DataTransfer();
|
||||||
|
const catValues = [];
|
||||||
|
let valid = true;
|
||||||
|
files.forEach(f => {
|
||||||
|
const cat = f.category || defaultCategory;
|
||||||
|
if (!cat) { valid = false; }
|
||||||
|
dt.items.add(f.file);
|
||||||
|
catValues.push(cat);
|
||||||
|
});
|
||||||
|
if (!valid) { alert('{{ __('admin.select_category_for_all') }}'); return; }
|
||||||
|
$refs.submitFiles.files = dt.files;
|
||||||
|
catValues.forEach((c, i) => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'hidden';
|
||||||
|
input.name = 'categories[' + i + ']';
|
||||||
|
input.value = c;
|
||||||
|
$el.closest('form').appendChild(input);
|
||||||
|
});
|
||||||
|
$el.closest('form').submit();
|
||||||
|
">
|
||||||
|
{{ __('admin.upload_files') }}
|
||||||
|
</button>
|
||||||
<a href="{{ route('admin.files.index') }}" class="text-sm text-gray-600 hover:underline">{{ __('ui.cancel') }}</a>
|
<a href="{{ route('admin.files.index') }}" class="text-sm text-gray-600 hover:underline">{{ __('ui.cancel') }}</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user