feat: implement citizen report creation, detail view, and chat system
- Create report creation page with step-by-step media uploads and categories loading - Create report details view page with history events log and messaging section - Implement MessageComposer and MessageList components for chat workflow - Implement MediaUploader with drag-and-drop support, file type, and size validation - Implement Pagination component with dynamic buttons and ellipsis support - Implement user profile settings summary page and notifications listing page
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue?: File[]
|
||||
maxFiles?: number
|
||||
acceptedTypes?: string
|
||||
maxFileSize?: number // in MB
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', files: File[]): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => [],
|
||||
maxFiles: 5,
|
||||
acceptedTypes: 'image/*,video/*',
|
||||
maxFileSize: 50, // 50MB
|
||||
disabled: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const fileInputRef = ref<HTMLInputElement>()
|
||||
const isDragging = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const files = computed({
|
||||
get: () => props.modelValue || [],
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const handleFilesSelected = (selectedFiles: FileList | null) => {
|
||||
if (!selectedFiles) return
|
||||
error.value = ''
|
||||
|
||||
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`
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
files.value = [...files.value, ...newFiles]
|
||||
}
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
files.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const openFileDialog = () => {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
const mb = Math.round(bytes / (1024 * 1024) * 10) / 10
|
||||
return mb > 0 ? `${mb}MB` : 'Piccolo'
|
||||
}
|
||||
|
||||
const getMediaType = (file: File): string => {
|
||||
return file.type.startsWith('video/') ? 'Video' : 'Immagine'
|
||||
}
|
||||
</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 }"
|
||||
@dragover.prevent="isDragging = true"
|
||||
@dragleave.prevent="isDragging = false"
|
||||
@drop.prevent="isDragging = false; handleFilesSelected(($event as DragEvent).dataTransfer?.files || null)"
|
||||
@click="!disabled && openFileDialog()"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<small class="text-muted d-block">
|
||||
Massimo {{ maxFiles }} file, fino a {{ maxFileSize }}MB per file
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Input nascosto -->
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
:multiple="props.maxFiles > 1"
|
||||
:accept="acceptedTypes"
|
||||
:disabled="disabled"
|
||||
style="display: none"
|
||||
@change="handleFilesSelected(($event.target as HTMLInputElement).files)"
|
||||
/>
|
||||
|
||||
<!-- Errori -->
|
||||
<div v-if="error" class="alert alert-danger mt-3 mb-0" 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>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-link p-0"
|
||||
@click="removeFile(idx)"
|
||||
:disabled="disabled"
|
||||
:aria-label="`Rimuovi ${file.name}`"
|
||||
>
|
||||
<BIIcon name="it-close-circle" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.upload-area {
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(.border-primary) {
|
||||
background-color: rgba(0, 111, 157, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.border-2 {
|
||||
border-width: 2px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user