feat: improve chat composer with file attachments and media previews

This commit is contained in:
2026-06-13 10:28:39 +02:00 Unverified
parent fcb2bc2f09
commit c42a68c1ed
4 changed files with 113 additions and 73 deletions
+49 -43
View File
@@ -15,7 +15,7 @@ const props = withDefaults(defineProps<Props>(), {
modelValue: () => [],
maxFiles: 5,
acceptedTypes: 'image/*,video/*',
maxFileSize: 50, // 50MB
maxFileSize: 50,
disabled: false
})
@@ -36,16 +36,14 @@ const handleFilesSelected = (selectedFiles: FileList | null) => {
const newFiles = Array.from(selectedFiles)
// Validazione numero file
if (files.value.length + newFiles.length > props.maxFiles) {
error.value = `Massimo ${props.maxFiles} file consentiti`
return
}
// Validazione dimensione
for (const file of newFiles) {
if (file.size > props.maxFileSize! * 1024 * 1024) {
error.value = `File ${file.name} supera il limite di ${props.maxFileSize}MB`
error.value = `Il file ${file.name} supera il limite di ${props.maxFileSize}MB`
return
}
}
@@ -66,17 +64,16 @@ const formatFileSize = (bytes: number): string => {
return mb > 0 ? `${mb}MB` : 'Piccolo'
}
const getMediaType = (file: File): string => {
return file.type.startsWith('video/') ? 'Video' : 'Immagine'
const getFileUrl = (file: File) => {
return URL.createObjectURL(file)
}
</script>
<template>
<div class="media-uploader">
<!-- Area trascinamento -->
<div
class="upload-area border-2 border-dashed rounded-3 p-4 text-center cursor-pointer"
:class="{ 'border-primary': isDragging, 'bg-light': !isDragging }"
class="upload-area border-2 border-dashed rounded-3 p-4 text-center cursor-pointer position-relative"
:class="{ 'border-primary bg-primary bg-opacity-10': isDragging, 'bg-light': !isDragging }"
@dragover.prevent="isDragging = true"
@dragleave.prevent="isDragging = false"
@drop.prevent="isDragging = false; handleFilesSelected(($event as DragEvent).dataTransfer?.files || null)"
@@ -84,54 +81,58 @@ const getMediaType = (file: File): string => {
>
<BIIcon name="it-upload" size="xl" color="primary" class="mb-3" />
<div class="h6 font-weight-bold">
Trascina i file qui o <span class="text-primary">clicca per selezionare</span>
Trascina i file qui o <span class="text-primary text-decoration-underline">clicca per selezionare</span>
</div>
<small class="text-muted d-block">
Massimo {{ maxFiles }} file, fino a {{ maxFileSize }}MB per file
<small class="text-muted d-block mt-2">
Massimo {{ maxFiles }} file ({{ maxFileSize }}MB max per file). Tipi: {{ acceptedTypes }}
</small>
</div>
<!-- Input nascosto -->
<input
ref="fileInputRef"
type="file"
:multiple="props.maxFiles > 1"
:multiple="maxFiles > 1"
:accept="acceptedTypes"
:disabled="disabled"
style="display: none"
class="d-none"
@change="handleFilesSelected(($event.target as HTMLInputElement).files)"
/>
<!-- Errori -->
<div v-if="error" class="alert alert-danger mt-3 mb-0" role="alert">
<div v-if="error" class="alert alert-danger mt-3 py-2 small" role="alert">
{{ error }}
</div>
<!-- Lista file -->
<div v-if="files.length > 0" class="mt-4">
<h6 class="font-weight-bold mb-3">File caricati ({{ files.length }}/{{ maxFiles }})</h6>
<div class="list-group">
<div v-for="(file, idx) in files" :key="`file-${idx}`" class="list-group-item d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-3">
<BIIcon
:name="file.type.startsWith('video/') ? 'it-video' : 'it-image'"
size="lg"
color="primary"
/>
<div>
<div class="font-weight-semibold small">{{ file.name }}</div>
<small class="text-muted">{{ getMediaType(file) }} • {{ formatFileSize(file.size) }}</small>
<div v-if="files.length > 0" class="mt-4 row g-3">
<div v-for="(file, idx) in files" :key="`file-${idx}`" class="col-6 col-md-4 col-lg-3">
<div class="card h-100 position-relative shadow-sm border-0">
<div class="ratio ratio-1x1 bg-light rounded-top overflow-hidden">
<img v-if="file.type.startsWith('image/')" :src="getFileUrl(file)" class="object-fit-cover" :alt="file.name" />
<video v-else-if="file.type.startsWith('video/')" :src="getFileUrl(file)" class="object-fit-cover"></video>
<div v-else class="d-flex align-items-center justify-content-center bg-secondary bg-opacity-25">
<BIIcon name="it-file" size="xl" color="secondary" />
</div>
</div>
<button
type="button"
class="btn btn-sm btn-link p-0"
@click="removeFile(idx)"
class="btn btn-danger btn-sm position-absolute top-0 end-0 m-1 rounded-circle p-1 d-flex align-items-center justify-content-center shadow"
style="width: 28px; height: 28px; z-index: 10;"
@click.stop="removeFile(idx)"
:disabled="disabled"
:aria-label="`Rimuovi ${file.name}`"
title="Rimuovi file"
>
<BIIcon name="it-close-circle" />
<BIIcon name="it-close" size="sm" color="white" />
</button>
<div class="card-body p-2 d-flex flex-column justify-content-between">
<div class="small text-truncate fw-semibold" :title="file.name">{{ file.name }}</div>
<div class="small text-muted d-flex justify-content-between align-items-center mt-1">
<span>{{ formatFileSize(file.size) }}</span>
<div class="progress flex-grow-1 ms-2" style="height: 4px;" title="Pronto per l'upload">
<div class="progress-bar bg-success w-100" role="progressbar"></div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -140,21 +141,26 @@ const getMediaType = (file: File): string => {
<style scoped>
.upload-area {
transition: all 0.3s ease;
cursor: pointer;
&:hover:not(.border-primary) {
background-color: rgba(0, 111, 157, 0.05);
}
transition: all 0.2s ease-in-out;
}
.upload-area:hover:not(.border-primary) {
background-color: #f8f9fa !important;
border-color: #0066cc !important;
}
.cursor-pointer {
cursor: pointer;
}
.border-dashed {
border-style: dashed !important;
}
.border-2 {
border-width: 2px !important;
}
.object-fit-cover {
object-fit: cover;
width: 100%;
height: 100%;
}
</style>