port-nimara-client-portal/components/FileUploader.vue

209 lines
5.1 KiB
Vue

<template>
<div>
<!-- Drop Zone -->
<div
class="drop-zone pa-8 text-center rounded-lg"
:class="{ 'drop-zone-active': isDragging }"
@drop="handleDrop"
@dragover.prevent="isDragging = true"
@dragleave.prevent="isDragging = false"
>
<v-icon size="64" color="primary" class="mb-4">
mdi-cloud-upload-outline
</v-icon>
<h3 class="text-h6 mb-2">Drag and drop files here</h3>
<p class="text-body-2 text-grey mb-4">or</p>
<v-btn
color="primary"
@click="openFileDialog"
prepend-icon="mdi-folder-open"
>
Browse Files
</v-btn>
<input
ref="fileInput"
type="file"
multiple
hidden
@change="handleFileSelect"
/>
<p class="text-caption text-grey mt-4">
Maximum file size: 50MB
</p>
</div>
<!-- Selected Files -->
<v-list v-if="selectedFiles.length > 0" class="mt-4">
<v-list-subheader>Selected Files ({{ selectedFiles.length }})</v-list-subheader>
<v-list-item
v-for="(file, index) in selectedFiles"
:key="index"
:title="file.name"
:subtitle="formatFileSize(file.size)"
>
<template v-slot:prepend>
<v-icon>{{ getFileIcon(file.name) }}</v-icon>
</template>
<template v-slot:append>
<v-btn
icon
variant="text"
size="small"
@click="removeFile(index)"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</template>
</v-list-item>
</v-list>
<!-- Upload Progress -->
<v-progress-linear
v-if="uploading && uploadProgress > 0"
:model-value="uploadProgress"
color="primary"
height="8"
class="mt-4"
/>
<!-- Actions -->
<v-card-actions v-if="selectedFiles.length > 0" class="mt-4">
<v-spacer />
<v-btn @click="clearFiles">Clear All</v-btn>
<v-btn
color="primary"
variant="flat"
@click="uploadFiles"
:loading="uploading"
:disabled="selectedFiles.length === 0"
>
Upload {{ selectedFiles.length }} File{{ selectedFiles.length > 1 ? 's' : '' }}
</v-btn>
</v-card-actions>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
interface Props {
uploading: boolean;
currentPath?: string;
}
interface Emits {
(e: 'upload', files: File[]): void;
(e: 'close'): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const selectedFiles = ref<File[]>([]);
const isDragging = ref(false);
const uploadProgress = ref(0);
const fileInput = ref<HTMLInputElement>();
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
// Handle drag and drop
const handleDrop = (e: DragEvent) => {
e.preventDefault();
isDragging.value = false;
const files = Array.from(e.dataTransfer?.files || []);
addFiles(files);
};
// Handle file selection
const handleFileSelect = (e: Event) => {
const input = e.target as HTMLInputElement;
const files = Array.from(input.files || []);
addFiles(files);
// Reset input value to allow selecting same file again
input.value = '';
};
// Add files to selection with validation
const addFiles = (files: File[]) => {
const validFiles = files.filter(file => {
if (file.size > MAX_FILE_SIZE) {
alert(`File "${file.name}" exceeds 50MB limit`);
return false;
}
return true;
});
selectedFiles.value = [...selectedFiles.value, ...validFiles];
};
// Remove file from selection
const removeFile = (index: number) => {
selectedFiles.value.splice(index, 1);
};
// Clear all files
const clearFiles = () => {
selectedFiles.value = [];
uploadProgress.value = 0;
};
// Upload files
const uploadFiles = () => {
if (selectedFiles.value.length === 0) return;
emit('upload', selectedFiles.value);
};
// Open file dialog
const openFileDialog = () => {
if (fileInput.value) {
fileInput.value.click();
}
};
// Helpers
const formatFileSize = (bytes: number): string => {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
};
const getFileIcon = (filename: string): string => {
const ext = filename.split('.').pop()?.toLowerCase() || '';
const iconMap: Record<string, string> = {
pdf: 'mdi-file-pdf-box',
doc: 'mdi-file-document',
docx: 'mdi-file-document',
xls: 'mdi-file-excel',
xlsx: 'mdi-file-excel',
jpg: 'mdi-file-image',
jpeg: 'mdi-file-image',
png: 'mdi-file-image',
gif: 'mdi-file-image',
svg: 'mdi-file-image',
zip: 'mdi-folder-zip',
rar: 'mdi-folder-zip',
txt: 'mdi-file-document-outline',
csv: 'mdi-file-delimited',
mp4: 'mdi-file-video',
mp3: 'mdi-file-music',
};
return iconMap[ext] || 'mdi-file';
};
</script>
<style scoped>
.drop-zone {
border: 2px dashed #ccc;
transition: all 0.3s;
background-color: #fafafa;
}
.drop-zone-active {
border-color: #1976d2;
background-color: rgba(25, 118, 210, 0.05);
}
</style>