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:
@@ -1,47 +1,119 @@
|
||||
<x-layouts.admin :title="__('admin.upload_file')">
|
||||
<h1 class="text-2xl font-bold mb-6">{{ __('admin.upload_file') }}</h1>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6 max-w-lg">
|
||||
<form method="POST" action="{{ route('admin.files.store') }}" enctype="multipart/form-data">
|
||||
<div class="bg-white rounded-lg shadow p-6 max-w-2xl">
|
||||
<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
|
||||
|
||||
{{-- Standard-Kategorie --}}
|
||||
<div class="mb-4">
|
||||
<label for="file_category_id" 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">
|
||||
<label for="default_category" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.file_category') }} *</label>
|
||||
<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>
|
||||
@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
|
||||
</select>
|
||||
@error('file_category_id')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||
</div>
|
||||
|
||||
{{-- Drag & Drop Zone --}}
|
||||
<div class="mb-4">
|
||||
<label for="file" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.upload_file') }} *</label>
|
||||
<div x-data="{ fileName: '', dragging: false }" class="relative">
|
||||
<div
|
||||
@dragover.prevent="dragging = true"
|
||||
@dragleave.prevent="dragging = false"
|
||||
@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="border-2 border-dashed rounded-lg p-8 text-center cursor-pointer hover:border-blue-400 transition-colors"
|
||||
@click="$refs.fileInput.click()">
|
||||
<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" />
|
||||
</svg>
|
||||
<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="!fileName">{{ __('admin.max_file_size') }}</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 || ''">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.select_files') }}</label>
|
||||
<div
|
||||
@dragover.prevent="dragging = true"
|
||||
@dragleave.prevent="dragging = false"
|
||||
@drop.prevent="dragging = false; addFiles($event.dataTransfer.files)"
|
||||
: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"
|
||||
@click="$refs.fileInput.click()">
|
||||
<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" />
|
||||
</svg>
|
||||
<p class="mt-2 text-sm text-gray-600" x-show="!files.length">{{ __('admin.drag_or_click_files') }}</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-2 text-sm font-medium text-blue-600" x-show="files.length" x-text="files.length + ' {{ __('admin.files_selected') }}'"></p>
|
||||
</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>
|
||||
|
||||
{{-- 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">
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user